From 5fca50792af59eebf1c27f8233d97dcb7d11ec2c Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:33:47 +0800 Subject: [PATCH 01/21] scripts: add hermetic cold-start benchmark and startup budgets bench-startup.py times spawn to --version/--help exit, REPL loop reached, -p first request at a loopback mock, and ACP initialize response, against a C-hello floor. Spawn counts come from a PATH-shim pass. Budgets gate on median/floor ratios and exact git/security spawn counts. Co-Authored-By: Codegraff --- docs/harness-latency.md | 50 +++ scripts/bench-startup.py | 781 +++++++++++++++++++++++++++++++++++ scripts/startup-budgets.json | 36 ++ 3 files changed, 867 insertions(+) create mode 100755 scripts/bench-startup.py create mode 100644 scripts/startup-budgets.json diff --git a/docs/harness-latency.md b/docs/harness-latency.md index 946d3c3a1..9f011f4cb 100644 --- a/docs/harness-latency.md +++ b/docs/harness-latency.md @@ -98,3 +98,53 @@ JSON containers and escaped string lengths directly, retaining the standard serializer where its formatting is needed. This preserves byte counts and context policy while avoiding unnecessary formatting. See [ADR 0115](adr/0115-context-estimates-count-json-bytes.md). Compare both payload kinds and the tool-loop fixture when changing this path. + +## Cold-start benchmark + +`scripts/bench-startup.py` times process startup, from spawn to each ready +point. It is hermetic: every sample gets a fresh copy of a temporary HOME and +an empty non-git cwd (`--git-repo` uses a one-commit repo with no remote). +The child environment is scrubbed to `TMPDIR`, `TERM`, a minimal `PATH` +(system dirs plus git; no `codedb-pro`/`muonry` unless `--companion`), and +`GRAFF_NO_TELEMETRY`, `GRAFF_NO_UPDATE_CHECK`, `GRAFF_AUTO_ISOLATE=off`, +`GRAFF_FLEET=off`, `GRAFF_NO_SMOLIFY`, `GRAFF_LEARN_AUTO=off` and `NO_COLOR`. +`security` is a stub that answers "not found", so the Keychain is never read. +The only network traffic is to the loopback mock on port 1234 (it must be free). + +| Point | Ready when | +|---|---| +| `a_version`, `a_help` | `--version` / `--help` exits (n ≥ 100) | +| `b_repl_ready` | the REPL loop is reached with stdin=/dev/null: the first `final-save` stamp. This needs `GRAFF_SHUTDOWN_DEBUG=1`, the only debug env in the timing pass | +| `c_first_request` | `-p hi --model lmstudio`: the mock receives the first POST | +| `d_acp_initialize` | `graff acp`: the `initialize` response line on stdout; `_nokey` is the pre-auth path | +| `floor` | `/usr/bin/true` and a compiled C hello, timed the same way | + +```sh +zig build graff -Doptimize=ReleaseSafe -Dlearn=false +cp zig-out/bin/graff /tmp/graff-before # immutable baseline +python3 scripts/bench-startup.py --graff /tmp/graff-before --out /tmp/before.json +# ...change, rebuild, then: +python3 scripts/bench-startup.py --out /tmp/after.json --compare /tmp/before.json \ + --budgets scripts/startup-budgets.json +python3 scripts/bench-startup.py --load /tmp/after.json --compare /tmp/before.json +``` + +The default is 3 warm-up rounds and 30 timed rounds. Points are interleaved, with +the order rotated each round, so drift is spread across all points. After the +timing pass, a `GRAFF_BOOT_DEBUG=1` pass records median per-phase ms +(`phases_ms`). A PATH-shim pass counts `git`, `security` and companion spawns +(`spawns`, `spawn_detail`) and fails the run if git fetches or adds a worktree. +Children are spawned with `os.posix_spawn`, so the timer adds about 1 ms of +interpreter cost, compared with about 2.7 ms for `subprocess`. Subtract +`floor.hello_ms.median` to get graff-only time; `--compare` prints this as +`net`. The `--budgets` gates are median/floor ratios plus exact spawn counts, +never absolute ms. `--env KEY=VALUE` adds knobs such as `GRAFF_NO_PLUGINS=1`. + +On macOS, git resolves through `xcrun` to the real binary. The `/usr/bin/git` +shim costs 70–110 ms on its first call under each new HOME, and every sample has +a new HOME. Run-to-run medians move by about 5–15% with host load, and the floor +moves too, so compare `net` values and gate on ratios. For small deltas, run the +baseline and candidate binaries back to back, several times, in alternating +order. These numbers exclude user plugins, the companion handshake (unless +`--companion`), the update check, auto-isolation, cold model catalogs and real +Keychain latency. Each of those adds to startup on a real HOME. diff --git a/scripts/bench-startup.py b/scripts/bench-startup.py new file mode 100755 index 000000000..01a0d8788 --- /dev/null +++ b/scripts/bench-startup.py @@ -0,0 +1,781 @@ +#!/usr/bin/env python3 +"""Hermetic cold-start benchmark for graff; see docs/harness-latency.md. + +Times four ready points from process spawn, with no network and no credentials: + a_version / a_help `--version` / `--help` exit (wall) + b_repl_ready line REPL reaches its loop (stdin=/dev/null; first + GRAFF_SHUTDOWN_DEBUG `final-save` stamp on stderr) + c_first_request `-p hi --model lmstudio`: first POST at the loopback mock + d_acp_initialize `graff acp`: the `initialize` response line on stdout + (`_nokey` variant: no credential, pre-auth path) +plus a floor (/usr/bin/true and a compiled C hello) timed the same way. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import platform +import queue +import re +import shutil +import signal +import socket +import statistics +import subprocess +import sys +import tempfile +import threading +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent / "eval")) +from mock_model import ScriptedModel # noqa: E402 + +clock = time.perf_counter_ns +SCHEMA = 1 +PORT = 1234 # the built-in lmstudio provider URL is fixed to 127.0.0.1:1234 +INIT = b'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}\n' +BOOT_LINE = re.compile(r"^\[boot \+(\d+)ms, phase (\d+)ms\] (.+)$") +# Safety vars are fixed; --env may not override them. +PROTECTED = {"HOME", "PATH", "GRAFF_NO_TELEMETRY", "GRAFF_NO_UPDATE_CHECK", "GRAFF_AUTO_ISOLATE", + "LMSTUDIO_API_KEY"} +# Git subcommands that reach a remote or mean auto-isolation fired. Any of them fails the run. +FORBIDDEN_GIT = ("fetch", "pull", "push", "clone", "ls-remote", "remote update", "worktree add") +LIVE = set() # process groups still to reap on exit + + +class BenchError(Exception): + pass + + +# ---------------------------------------------------------------- processes + +def spawn(argv, env, cwd, stdin=None, stdout=None, stderr=None): + """os.posix_spawn in its own process group. ~1 ms floor versus ~2.7 ms for + subprocess on macOS, so less interpreter overhead lands in the timer. + stdin/stdout/stderr: None for /dev/null, else an fd to dup2.""" + actions = [] + for target, fd in ((0, stdin), (1, stdout), (2, stderr)): + if fd is None: + flags = os.O_RDONLY if target == 0 else os.O_WRONLY + actions.append((os.POSIX_SPAWN_OPEN, target, os.devnull, flags, 0)) + else: + actions.append((os.POSIX_SPAWN_DUP2, fd, target)) + os.chdir(cwd) # posix_spawn has no chdir action before Python 3.13 + pid = os.posix_spawn(argv[0], argv, env, file_actions=actions, setpgroup=0) + LIVE.add(pid) + return pid + + +def reap_group(pid): + try: + os.killpg(pid, signal.SIGKILL) + except OSError: + pass + LIVE.discard(pid) + + +class Watchdog: + """Kills the process group at the deadline so a stuck run cannot hang.""" + def __init__(self, timeout): + self.fired = False + self.pid = None + self.timer = threading.Timer(timeout, self._fire) + self.timer.daemon = True + self.timer.start() + + def _fire(self): + self.fired = True + if self.pid is not None: + reap_group(self.pid) + + def cancel(self): + self.timer.cancel() + + +def wait_child(pid, dog): + _, status, usage = os.wait4(pid, 0) + ended = clock() + dog.cancel() + reap_group(pid) # stray grandchildren (git, companions) must not outlive the sample + rc = os.waitstatus_to_exitcode(status) if hasattr(os, "waitstatus_to_exitcode") else ( + -os.WTERMSIG(status) if os.WIFSIGNALED(status) else os.WEXITSTATUS(status)) + return ended, rc, (usage.ru_utime + usage.ru_stime) * 1e3 + + +def tail(path, limit=1200): + try: + return Path(path).read_text(errors="replace")[-limit:] + except OSError: + return "" + + +def run_wall(argv, env, cwd, timeout, errfile): + """Spawn → exit. stdout to /dev/null, stderr to errfile (for failure reports).""" + errfd = os.open(errfile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + dog = Watchdog(timeout) + try: + started = clock() + pid = dog.pid = spawn(argv, env, cwd, stderr=errfd) + except OSError: + dog.cancel() + raise + finally: + os.close(errfd) + ended, rc, cpu = wait_child(pid, dog) + if dog.fired: + raise BenchError(f"{' '.join(argv[1:]) or argv[0]}: no exit within {timeout}s\n{tail(errfile)}") + return {"started": started, "wall_ms": (ended - started) / 1e6, "cpu_ms": cpu, "rc": rc} + + +def _reader(fd, tag, events): + buf = b"" + while True: + try: + chunk = os.read(fd, 65536) + except OSError: + chunk = b"" + stamp = clock() + if not chunk: + break + buf += chunk + while b"\n" in buf: + line, buf = buf.split(b"\n", 1) + events.put((stamp, tag, line.decode(errors="replace"))) + if buf: + events.put((clock(), tag, buf.decode(errors="replace"))) + events.put((clock(), tag, None)) + os.close(fd) + + +def run_stream(argv, env, cwd, timeout, ready, stdin_data=None): + """Spawn, optionally write stdin_data (stdin stays open), and stamp the first + line where ready(tag, line) holds. Then close stdin and time the exit.""" + events = queue.Queue() + out_r, out_w = os.pipe() + err_r, err_w = os.pipe() + in_r, in_w = os.pipe() if stdin_data is not None else (None, None) + readers = [threading.Thread(target=_reader, args=(fd, tag, events), daemon=True) + for fd, tag in ((out_r, "out"), (err_r, "err"))] + for thread in readers: + thread.start() + dog = Watchdog(timeout) + try: + started = clock() + pid = dog.pid = spawn(argv, env, cwd, stdin=in_r, stdout=out_w, stderr=err_w) + except OSError: + dog.cancel() # readers see EOF once the finally below closes the write ends + if in_w is not None: + os.close(in_w) + raise + finally: + for fd in (out_w, err_w, in_r): + if fd is not None: + os.close(fd) + deadline = time.monotonic() + timeout + lines, ready_at, eofs = [], None, 0 + try: + if stdin_data is not None: + os.write(in_w, stdin_data) + while ready_at is None and eofs < 2: + try: + stamp, tag, line = events.get(timeout=max(0.0, deadline - time.monotonic())) + except queue.Empty: + break + if line is None: + eofs += 1 + continue + lines.append((stamp, tag, line)) + if ready(tag, line): + ready_at = stamp + finally: + if in_w is not None: + try: + os.close(in_w) + except OSError: + pass + if ready_at is None: + reap_group(pid) # unblock wait4 now rather than at the watchdog deadline + ended, rc, cpu = wait_child(pid, dog) + for thread in readers: + thread.join(timeout=2) + while True: + try: + stamp, tag, line = events.get_nowait() + except queue.Empty: + break + if line is not None: + lines.append((stamp, tag, line)) + stderr_tail = "\n".join(l for _, t, l in lines if t == "err")[-1200:] + if ready_at is None: + why = "timed out" if time.monotonic() >= deadline or dog.fired else f"exited rc={rc}" + raise BenchError(f"{' '.join(argv[1:])}: ready marker never appeared ({why})\n{stderr_tail}") + return {"started": started, "ready_ms": (ready_at - started) / 1e6, + "wall_ms": (ended - started) / 1e6, "cpu_ms": cpu, "rc": rc, "lines": lines} + + +# ---------------------------------------------------------------- fixtures + +class Mock(ScriptedModel): + """scripts/eval/mock_model.py, plus a perf_counter_ns stamp per POST. The + stamp is taken once the request body has been read (loopback, sub-ms).""" + def __init__(self): + super().__init__([]) + self.posts = [] + + def next_reply(self, body): + stamp = clock() + with self._lock: + self.posts.append(stamp) + return super().next_reply(body) + + +def start_mock(): + probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + probe.settimeout(0.3) + try: + if probe.connect_ex(("127.0.0.1", PORT)) == 0: + raise BenchError(f"127.0.0.1:{PORT} is busy (LM Studio or another mock?). The lmstudio " + "provider is fixed to that port; stop the listener and rerun.") + finally: + probe.close() + mock = Mock() + try: + mock.start(PORT) + except OSError as exc: + raise BenchError(f"cannot bind 127.0.0.1:{PORT}: {exc}") from exc + return mock + + +def which_in(name, path): + found = shutil.which(name, path=path) + return os.path.realpath(found) if found else None + + +def write_exe(path, text): + path.write_text(text) + path.chmod(0o755) + + +class Fixture: + """Scratch tree: bin/ (timing PATH), shim/ (spawn-logging PATH), template + HOME and cwd per point. Each sample runs in fresh copies of the templates.""" + def __init__(self, args): + self.root = Path(tempfile.mkdtemp(prefix="graff-startup-")).resolve() + try: + self._setup(args) + except BaseException: + self.cleanup() + raise + + def _setup(self, args): + self.args = args + user_path = os.environ.get("PATH", "") + self.git = which_in("git", user_path) + if not self.git: + raise BenchError("git not found on PATH") + if sys.platform == "darwin" and self.git == "/usr/bin/git": + # /usr/bin/git is the xcrun shim: its first call under each new HOME + # costs ~70-110 ms (then ~11 ms). Every sample has a fresh HOME, so + # the shim would bill that to graff on every run. Use the real git. + found = subprocess.run(["xcrun", "--find", "git"], capture_output=True, text=True).stdout.strip() + if found and os.access(found, os.X_OK): + self.git = found + self.git_version = subprocess.run([self.git, "--version"], capture_output=True, + text=True).stdout.strip() + self.bin = self.root / "bin" + self.shim = self.root / "shim" + self.log = self.root / "spawns.log" + self.bin.mkdir() + self.shim.mkdir() + os.symlink(self.git, self.bin / "git") + # Never reach the real Keychain: answer "not found" (44) like an empty one. + stub = "#!/bin/sh\n{log}echo 'security: The specified item could not be found in the keychain.' >&2\nexit 44\n" + write_exe(self.bin / "security", stub.format(log="")) + write_exe(self.shim / "security", stub.format(log=f'printf "%s\\n" "security $*" >> "{self.log}"\n')) + write_exe(self.shim / "git", f'#!/bin/sh\nprintf "%s\\n" "git $*" >> "{self.log}"\nexec "{self.git}" "$@"\n') + self.companions = {} + if args.companion: + for name in ("codedb-pro", "muonry"): + real = which_in(name, user_path) + if real: + self.companions[name] = real + os.symlink(real, self.bin / name) + write_exe(self.shim / name, f'#!/bin/sh\nprintf "%s\\n" "{name} $*" >> "{self.log}"\nexec "{real}" "$@"\n') + if "codedb-pro" not in self.companions: + raise BenchError("--companion needs codedb-pro on PATH") + self.path = os.pathsep.join([str(self.bin), "/usr/bin", "/bin", "/usr/sbin", "/sbin"]) + self.shim_path = str(self.shim) + os.pathsep + self.path + self.hello = None + cc = which_in("cc", user_path) + if cc: + (self.root / "hello.c").write_text('#include \nint main(void){write(1,"hi\\n",3);return 0;}\n') + if subprocess.run([cc, "-O2", "-o", str(self.root / "hello"), str(self.root / "hello.c")], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: + self.hello = str(self.root / "hello") + self.counter = 0 + + def env(self, home, key=True, extra=None, path=None): + env = {k: os.environ[k] for k in ("TMPDIR", "TERM") if k in os.environ} + env.update(PATH=path or self.path, HOME=str(home), GRAFF_NO_TELEMETRY="1", + GRAFF_NO_UPDATE_CHECK="1", GRAFF_AUTO_ISOLATE="off", GRAFF_FLEET="off", + GRAFF_NO_SMOLIFY="1", GRAFF_LEARN_AUTO="off", NO_COLOR="1") + if key: + env["LMSTUDIO_API_KEY"] = "local" + env.update(self.args.extra_env) + env.update(extra or {}) + return env + + def git_env(self, home): + return dict(self.env(home, key=False), GIT_CONFIG_NOSYSTEM="1") + + def template(self, name): + base = self.root / "templates" / name + home, work = base / "home", base / "work" + home.mkdir(parents=True) + work.mkdir() + genv = self.git_env(home) + if self.args.git_repo: + (work / "README.md").write_text("startup benchmark fixture\n") + for cmd in (["init", "-q"], ["add", "README.md"], + ["-c", "user.name=bench", "-c", "user.email=bench@example.invalid", + "-c", "commit.gpgsign=false", "commit", "-q", "-m", "fixture"]): + subprocess.run([self.git] + cmd, cwd=work, env=genv, check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + else: + inside = subprocess.run([self.git, "rev-parse", "--is-inside-work-tree"], cwd=work, env=genv, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + if inside: + raise BenchError(f"scratch dir {work} is inside a git repository; set TMPDIR elsewhere") + return home, work + + def fresh(self, template): + """Copy a template HOME+cwd so every sample sees identical state.""" + self.counter += 1 + dest = self.root / "runs" / str(self.counter) + home = dest / "home" + work = dest / "work" + shutil.copytree(template[0], home, symlinks=True) + shutil.copytree(template[1], work, symlinks=True) + return dest, home, work + + def cleanup(self): + shutil.rmtree(self.root, ignore_errors=True) + + +# ---------------------------------------------------------------- points + +def acp_ready(tag, line): + if tag != "out": + return False + try: + message = json.loads(line) + except ValueError: + return False + return message.get("id") == 1 and ("result" in message or "error" in message) + + +def repl_ready(tag, line): + return tag == "err" and line.startswith("[shutdown") and "final-save" in line + + +POINTS = { + # name: argv tail, kind, metric names, key, extra env, ready predicate, stdin + "a_version": (["--version"], "wall", True, {}, None, None), + "a_help": (["--help"], "wall", True, {}, None, None), + # The only REPL "loop reached" stamp is the shutdown trace; acceptable debug env. + "b_repl_ready": (["--model", "lmstudio"], "stream", True, {"GRAFF_SHUTDOWN_DEBUG": "1"}, repl_ready, None), + "c_first_request": (["-p", "hi", "--model", "lmstudio"], "post", True, {}, None, None), + "d_acp_initialize": (["acp", "--model", "lmstudio"], "stream", True, {}, acp_ready, INIT), + "d_acp_initialize_nokey": (["acp"], "stream", False, {}, acp_ready, INIT), +} +READY_METRIC = {"b_repl_ready": "ready_ms", "c_first_request": "request_ms", + "d_acp_initialize": "response_ms", "d_acp_initialize_nokey": "response_ms"} + + +def sample(fx, mock, binary, name, template, timeout, extra=None, path=None): + tail_argv, kind, key, env_extra, ready, stdin_data = POINTS[name] + dest, home, work = fx.fresh(template) + env = fx.env(home, key=key, extra=dict(env_extra, **(extra or {})), path=path) + argv = [binary] + tail_argv + with mock._lock: + before = len(mock.posts) + try: + if kind == "stream": + got = run_stream(argv, env, str(work), timeout, ready, stdin_data) + row = {READY_METRIC[name]: got["ready_ms"], "wall_ms": got["wall_ms"], "cpu_ms": got["cpu_ms"]} + lines = got["lines"] + if name.startswith("d_"): + reply = next(json.loads(l) for _, t, l in lines if t == "out" and acp_ready(t, l)) + if "result" not in reply: + raise BenchError(f"{name}: initialize returned an error: {reply.get('error')}") + else: + got = run_wall(argv, env, str(work), timeout, str(dest / "stderr")) + if got["rc"] != 0: + raise BenchError(f"{name}: exit {got['rc']}\n{tail(dest / 'stderr')}") + row = {"wall_ms": got["wall_ms"], "cpu_ms": got["cpu_ms"]} + lines = [(0, "err", l) for l in tail(dest / "stderr", 1 << 20).splitlines()] + with mock._lock: + posts = mock.posts[before:] + if kind == "post": + if not posts: + raise BenchError(f"{name}: the mock never received a request\n{tail(dest / 'stderr')}") + row["request_ms"] = (posts[0] - got["started"]) / 1e6 + elif posts: + raise BenchError(f"{name}: unexpected model request ({len(posts)}) during startup") + row["posts"] = len(posts) + return row, lines + finally: + shutil.rmtree(dest, ignore_errors=True) + + +def floor_sample(fx, prog, timeout): + work = fx.root / "floorcwd" + work.mkdir(exist_ok=True) + return run_wall([prog], fx.env(fx.root), str(work), timeout, str(fx.root / "floor.err"))["wall_ms"] + + +def stats(values): + ordered = sorted(values) + if not ordered: + return {"n": 0} + pick = lambda q: ordered[max(0, math.ceil(len(ordered) * q) - 1)] # noqa: E731 + return {"n": len(ordered), "min": round(ordered[0], 3), "median": round(statistics.median(ordered), 3), + "p95": round(pick(0.95), 3), "max": round(ordered[-1], 3)} + + +def parse_spawns(log, roots): + counts, detail = {}, [] + if log.exists(): + for line in log.read_text(errors="replace").splitlines(): + tool = line.split(" ", 1)[0] + counts[tool] = counts.get(tool, 0) + 1 + for root in roots: + line = line.replace(root, "") + detail.append(line) + return counts, detail + + +def measure(args, binary): + mock = start_mock() + fx = None + try: + fx = Fixture(args) + names = [n for n in POINTS if n in args.points] + templates = {n: fx.template(n) for n in names if not n.startswith("a_")} + a_template = fx.template("a") + for n in names: + templates.setdefault(n, a_template) + per_round = {n: (math.ceil(100 / args.runs) if n.startswith("a_") else 1) for n in names} + floors = [("true_ms", "/usr/bin/true")] + ([("hello_ms", fx.hello)] if fx.hello else []) + samples = {n: {} for n in names} + floor_samples = {k: [] for k, _ in floors} + load = [os.getloadavg()[0]] + + def log(msg): + if not args.quiet: + print(msg, file=sys.stderr, flush=True) + + # Warm-up runs in the templates so each copied sample sees steady state. + for _ in range(args.warmup): + for n in names: + tail_argv, kind, key, env_extra, ready, stdin_data = POINTS[n] + home, work = templates[n] + env = fx.env(home, key=key, extra=env_extra) + if kind == "stream": + run_stream([binary] + tail_argv, env, str(work), args.timeout, ready, stdin_data) + else: + run_wall([binary] + tail_argv, env, str(work), args.timeout, str(fx.root / "warm.err")) + for _, prog in floors: + floor_sample(fx, prog, args.timeout) + # Timing pass: interleaved rounds, rotated order, no debug env except b's marker. + order = [("floor", k, p) for k, p in floors] + [("point", n, None) for n in names] + for r in range(args.runs): + rotated = order[r % len(order):] + order[:r % len(order)] + for kind, n, prog in rotated: + if kind == "floor": + for _ in range(math.ceil(50 / args.runs)): + floor_samples[n].append(floor_sample(fx, prog, args.timeout)) + continue + for _ in range(per_round[n]): + row, _ = sample(fx, mock, binary, n, templates[n], args.timeout) + for metric, value in row.items(): + samples[n].setdefault(metric, []).append(value) + log(f"round {r + 1}/{args.runs}") + load.append(os.getloadavg()[0]) + points = {} + for n in names: + points[n] = {m: stats(v) for m, v in samples[n].items() if m != "posts"} + points[n]["model_requests"] = max(samples[n].get("posts", [0])) + # Diagnostic pass: per-phase medians from GRAFF_BOOT_DEBUG. + for n in names: + if n.startswith("a_") or args.diag_runs <= 0: + continue + phases = {} + for _ in range(args.diag_runs): + _, lines = sample(fx, mock, binary, n, templates[n], args.timeout, {"GRAFF_BOOT_DEBUG": "1"}) + seen = {} + for _, tag, line in lines: + match = BOOT_LINE.match(line) if tag == "err" else None + if match: + seen[match.group(3)] = seen.get(match.group(3), 0) + int(match.group(2)) + for label, ms in seen.items(): + phases.setdefault(label, []).append(ms) + points[n]["phases_ms"] = {k: statistics.median(v) for k, v in phases.items()} + log(f"diag {n}") + # Spawn pass: PATH shims log every git/security/companion spawn. + roots = [str(fx.root), os.path.realpath(str(fx.root))] + for n in names: + runs = [] + for _ in range(max(1, args.shim_runs)): + if fx.log.exists(): + fx.log.unlink() + sample(fx, mock, binary, n, templates[n], args.timeout, path=fx.shim_path) + runs.append(parse_spawns(fx.log, roots)) + tools = ["git", "security"] + sorted(fx.companions) + counts = {t: max(c.get(t, 0) for c, _ in runs) for t in tools} + points[n]["spawns"] = counts + if any(c != runs[0][0] for c, _ in runs): + points[n]["spawns_nondeterministic"] = [c for c, _ in runs] + points[n]["spawn_detail"] = runs[0][1] + for line in runs[0][1]: + if line.startswith("git ") and any(f" {cmd}" in line for cmd in FORBIDDEN_GIT): + raise BenchError(f"{n}: network/auto-isolation git command observed: {line}") + log(f"spawns {n}: {counts}") + return {"floor": {k: stats(v) for k, v in floor_samples.items()}, "points": points, + "samples": {"floor": floor_samples, **samples}, "load": load, + "path_dirs": ["/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"], + "companions": sorted(fx.companions), "git": fx.git_version} + finally: + for pid in list(LIVE): + reap_group(pid) + mock.stop() + if fx is not None: + fx.cleanup() + + +# ---------------------------------------------------------------- report + +def cpu_name(): + try: + if sys.platform == "darwin": + return subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"], capture_output=True, + text=True, timeout=5).stdout.strip() + for line in Path("/proc/cpuinfo").read_text().splitlines(): + if line.startswith("model name"): + return line.split(":", 1)[1].strip() + except (OSError, subprocess.SubprocessError): + pass + return platform.processor() + + +def floor_ref(result): + floor = result.get("floor", {}) + for key in ("hello_ms", "true_ms"): + if floor.get(key, {}).get("n"): + return key, floor[key]["median"] + return None, None + + +def headline(result): + rows = [] + for name, point in result["points"].items(): + for metric, value in point.items(): + if isinstance(value, dict) and "median" in value and metric != "cpu_ms": + rows.append((f"{name}.{metric}", value)) + return rows + + +def print_summary(result): + key, floor = floor_ref(result) + print(f"floor {key}: median {floor} ms (subtract for graff-only cost)" if floor else "floor: none") + for k, v in result["floor"].items(): + print(f" {k:28} n={v['n']:3} min {v['min']:8.2f} med {v['median']:8.2f} p95 {v['p95']:8.2f}") + for label, v in headline(result): + print(f" {label:44} n={v['n']:3} min {v['min']:8.2f} med {v['median']:8.2f} p95 {v['p95']:8.2f} max {v['max']:8.2f}") + for name, point in result["points"].items(): + if "spawns" in point: + print(f" {name:44} spawns {point['spawns']}") + + +def variant(result): + env = result.get("env", {}) + return "git_repo" if env.get("git_repo") else "default" + + +def compare(before, after): + if variant(before) != variant(after) or before["env"].get("companion") != after["env"].get("companion"): + print("WARNING: comparing different variants (git_repo/companion differ)") + if before.get("host", {}).get("cpu") != after.get("host", {}).get("cpu"): + print("WARNING: different hosts") + _, fb = floor_ref(before) + _, fa = floor_ref(after) + print(f"{'point.metric':44} {'before':>9} {'after':>9} {'delta':>9} {'delta%':>7} net(before→after)") + old = dict(headline(before)) + for label, v in headline(after): + if label not in old: + continue + b, a = old[label]["median"], v["median"] + net = f"{b - (fb or 0):7.2f}→{a - (fa or 0):.2f}" + pct = (a - b) / b * 100 if b else 0.0 + print(f"{label:44} {b:9.2f} {a:9.2f} {a - b:+9.2f} {pct:+6.1f}% {net}") + for name, point in after["points"].items(): + prev = before["points"].get(name, {}).get("spawns") + if prev is not None and "spawns" in point and prev != point["spawns"]: + print(f"{name + ' spawns':44} {prev} → {point['spawns']}") + + +def check_budgets(result, budgets): + failures, notes = [], [] + key, floor = floor_ref(result) + if budgets.get("floor") and budgets["floor"] != key: + notes.append(f"budget floor {budgets['floor']} unavailable; using {key}") + for label, rule in budgets.get("ratio_over_floor", {}).items(): + if label.startswith("_"): + continue + name, metric = label.split(".", 1) + stat = result["points"].get(name, {}).get(metric) + if not stat or not floor: + notes.append(f"skip {label}: not measured") + continue + ratio = stat["median"] / floor + status = "ok" if ratio <= rule["max"] else "FAIL" + print(f"budget {label:40} ratio {ratio:6.1f} ≤ {rule['max']:<6} {status}") + if status != "ok": + failures.append(label) + for name, tools in budgets.get("spawns", {}).get(variant(result), {}).items(): + if name.startswith("_"): + continue + point = result["points"].get(name, {}) + if "spawns" not in point: + notes.append(f"skip {name} spawns: not measured") + continue + for tool, rule in tools.items(): + if tool.startswith("_"): + continue + limit = rule["max"] if isinstance(rule, dict) else rule + got = point["spawns"].get(tool, 0) + status = "ok" if got <= limit else "FAIL" + print(f"budget {name + ' ' + tool + ' spawns':40} {got:6} ≤ {limit:<6} {status}") + if status != "ok": + failures.append(f"{name}.{tool}") + for note in notes: + print(note) + return failures + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + root = Path(__file__).resolve().parents[1] + parser.add_argument("--graff", type=Path, default=root / "zig-out/bin/graff") + parser.add_argument("--runs", type=int, default=30, help="timed rounds (a_* points take ≥100 samples)") + parser.add_argument("--warmup", type=int, default=3) + parser.add_argument("--diag-runs", type=int, default=5, help="GRAFF_BOOT_DEBUG runs per point (0 to skip)") + parser.add_argument("--shim-runs", type=int, default=2, help="spawn-counting runs per point") + parser.add_argument("--timeout", type=float, default=30, help="per-run deadline in seconds") + parser.add_argument("--points", default=",".join(POINTS), help="comma-separated subset") + parser.add_argument("--git-repo", action="store_true", help="cwd is a throwaway one-commit repo, no remote") + parser.add_argument("--companion", action="store_true", help="put codedb-pro/muonry on PATH") + parser.add_argument("--env", action="append", default=[], metavar="KEY=VALUE", + help="extra child env (knob experiments); safety vars cannot be overridden") + parser.add_argument("--out", type=Path, help="write the result JSON here") + parser.add_argument("--load", type=Path, help="use an existing result instead of measuring") + parser.add_argument("--compare", type=Path, help="print deltas against an earlier result") + parser.add_argument("--budgets", type=Path, help="gate on ratio-over-floor and spawn counts") + parser.add_argument("--quiet", action="store_true") + args = parser.parse_args() + args.extra_env = {} + for item in args.env: + k, sep, v = item.partition("=") + if not sep or not k: + parser.error(f"--env wants KEY=VALUE, got {item!r}") + if k in PROTECTED: + parser.error(f"--env may not override {k}") + args.extra_env[k] = v + args.points = [p.strip() for p in args.points.split(",") if p.strip()] + unknown = [p for p in args.points if p not in POINTS] + if unknown: + parser.error(f"unknown points {unknown}; choose from {list(POINTS)}") + if args.runs < 1 or args.warmup < 0 or args.timeout <= 0: + parser.error("runs ≥ 1, warmup ≥ 0, timeout > 0") + + if args.load: + result = json.loads(args.load.read_text()) + else: + if not args.out: + parser.error("--out is required unless --load is given") + binary = os.path.realpath(str(args.graff)) + if not os.path.isfile(binary) or not os.access(binary, os.X_OK): + raise BenchError(f"graff binary not found or not executable: {args.graff} " + "(build with: zig build graff -Doptimize=ReleaseSafe -Dlearn=false)") + data = Path(binary).read_bytes() + digest = hashlib.sha256(data).hexdigest() + cwd = os.getcwd() + try: + try: + with tempfile.TemporaryDirectory(prefix="graff-startup-version-") as scratch: + version = subprocess.run( + [binary, "--version"], capture_output=True, text=True, timeout=10, cwd=scratch, + env={"PATH": "/usr/bin:/bin", "HOME": scratch, "GRAFF_NO_TELEMETRY": "1", + "GRAFF_NO_UPDATE_CHECK": "1", "GRAFF_AUTO_ISOLATE": "off"}, + ).stdout.strip().splitlines()[:1] + except subprocess.TimeoutExpired as exc: + raise BenchError(f"{binary} --version did not exit within 10s") from exc + measured = measure(args, binary) + finally: + os.chdir(cwd) + if hashlib.sha256(Path(binary).read_bytes()).hexdigest() != digest: + raise BenchError("binary changed during measurement; rerun against a stable copy") + result = { + "schema": SCHEMA, + "graff": {"path": binary, "sha256": digest, "size": len(data), "version": (version or [""])[0]}, + "host": {"os": platform.system(), "release": platform.release(), "arch": platform.machine(), + "cpu": cpu_name(), "ncpu": os.cpu_count(), "python": platform.python_version(), + "load1": measured["load"]}, + "env": {"hermetic": True, "git_repo": args.git_repo, "companion": args.companion, + "companions_found": measured["companions"], "auto_isolate": "off", + "git": measured["git"], + "extra_env": sorted(args.extra_env), "path": measured["path_dirs"], + "security": "stub (exit 44, Keychain never read)", + "debug_env": {"b_repl_ready": "GRAFF_SHUTDOWN_DEBUG=1 (ready marker)", + "phases_ms": "GRAFF_BOOT_DEBUG=1 (separate diagnostic pass)"}}, + "timer": {"method": "os.posix_spawn + wait4, perf_counter_ns from before spawn", + "note": "includes the interpreter's spawn cost; subtract floor.*.median for graff-only time"}, + "runs": args.runs, "warmup": args.warmup, "diag_runs": args.diag_runs, "shim_runs": args.shim_runs, + "floor": measured["floor"], "points": measured["points"], "samples": measured["samples"], + } + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(result, indent=1) + "\n") + print_summary(result) + if args.compare: + print() + compare(json.loads(args.compare.read_text()), result) + if args.budgets: + print() + failures = check_budgets(result, json.loads(args.budgets.read_text())) + if failures: + print(f"startup budgets exceeded: {', '.join(failures)}") + return 1 + return 0 + + +def _terminate(signum, _frame): + raise SystemExit(128 + signum) + + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, _terminate) + try: + code = main() + except BenchError as exc: + print(f"bench-startup: {exc}", file=sys.stderr) + code = 2 + except KeyboardInterrupt: + code = 130 + finally: + for pid in list(LIVE): + reap_group(pid) + sys.exit(code) diff --git a/scripts/startup-budgets.json b/scripts/startup-budgets.json new file mode 100644 index 000000000..aa996729c --- /dev/null +++ b/scripts/startup-budgets.json @@ -0,0 +1,36 @@ +{ + "schema": 1, + "_about": "Gates for scripts/bench-startup.py --budgets. Time gates are median / floor median (floor = compiled C hello), not absolute ms, so a slower or busier machine scales both sides. Ratio maxima are about 2.5x the baseline ratio so they hold under load; tighten them after an optimization lands. Spawn gates are exact counts from the PATH-shim pass and do not depend on load. 'target' is the planned value once the startup work lands; only 'max' is enforced.", + "floor": "hello_ms", + "ratio_over_floor": { + "_baseline": "Ratios measured on the unchanged baseline, hermetic, non-git cwd: a_version 1.6, a_help 1.5, b ready 10.3, c request 3.7, c wall 5.0, d response 10.2, d no-key 10.3. The --git-repo ratios are similar.", + "a_version.wall_ms": {"max": 4}, + "a_help.wall_ms": {"max": 4}, + "b_repl_ready.ready_ms": {"max": 26, "target": 6}, + "b_repl_ready.wall_ms": {"max": 28}, + "c_first_request.request_ms": {"max": 10}, + "c_first_request.wall_ms": {"max": 13}, + "d_acp_initialize.response_ms": {"max": 26, "target": 4}, + "d_acp_initialize_nokey.response_ms": {"max": 26} + }, + "spawns": { + "default": { + "_tighten": "git spawns per REPL/ACP boot come from presence.announce -> worktree_lease.currentIdentity (2 sequential rev-parse). Tighten to max 1 when the identity is memoized and uses one spawn, and to 0 once it is resolved in-process. Tighten security on the no-key path once there is a Keychain index.", + "a_version": {"git": 0, "security": 0}, + "a_help": {"git": 0, "security": 0}, + "b_repl_ready": {"git": {"max": 2, "target": 0}, "security": 0}, + "c_first_request": {"git": 0, "security": 0}, + "d_acp_initialize": {"git": {"max": 2, "target": 0}, "security": 0}, + "d_acp_initialize_nokey": {"git": 0, "security": {"max": 26, "target": 0}} + }, + "git_repo": { + "_tighten": "Same as default. With GRAFF_AUTO_ISOLATE=off (always set by the benchmark) a one-commit repo also costs 2 git spawns per REPL/ACP boot.", + "a_version": {"git": 0, "security": 0}, + "a_help": {"git": 0, "security": 0}, + "b_repl_ready": {"git": {"max": 2, "target": 1}, "security": 0}, + "c_first_request": {"git": 0, "security": 0}, + "d_acp_initialize": {"git": {"max": 2, "target": 1}, "security": 0}, + "d_acp_initialize_nokey": {"git": 0, "security": {"max": 26, "target": 0}} + } + } +} From 3016f3c370eed9bfcbd6ecb169df53e2d25021a9 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:46:43 +0800 Subject: [PATCH 02/21] boot: resolve worktree identity in-process; gate the boot reap currentIdentity spawned two git rev-parse per call and ran three times per boot; maybeAutoIsolate spawned rev-parse --is-inside-work-tree before its lean check; the boot reap ran worktree prune + list whenever .graff/worktrees existed. REPL/ACP in a repo paid 7-9 git spawns before ready, -p paid 1. - git_dirs: answer --git-dir/--git-common-dir from the filesystem for a .git directory or gitdir file, mirroring git's discovery (filesystem boundary, HEAD/objects/refs, ownership, repo format). Discovery env vars, bare/inside-git-dir, symlinks, extensions/includes and anything unreadable fall back to git. - worktree_lease: the fallback is one rev-parse for both dirs, memoized per getcwd and invalidated on chdir (task_workspace.enter, presence.rebind). identityAt uses the same path. - maybeAutoIsolate: return before any git work for lean/isolated runs and derive is_git from the identity. - worktree_reap.orphansAtBoot: skip the git spawns when no registration is prunable and no listed tree is a dead session's. Co-Authored-By: Codegraff --- src/git_dirs.zig | 265 +++++++++++++++++++++++++++++++++++ src/git_dirs_tests.zig | 213 ++++++++++++++++++++++++++++ src/main.zig | 2 +- src/presence.zig | 2 +- src/session_start.zig | 2 +- src/task_workspace.zig | 6 +- src/task_workspace_tests.zig | 36 +++++ src/worktree_lease.zig | 112 +++++++++++++-- src/worktree_reap.zig | 10 ++ src/worktree_reap_gate.zig | 118 ++++++++++++++++ 10 files changed, 752 insertions(+), 14 deletions(-) create mode 100644 src/git_dirs.zig create mode 100644 src/git_dirs_tests.zig create mode 100644 src/worktree_reap_gate.zig diff --git a/src/git_dirs.zig b/src/git_dirs.zig new file mode 100644 index 000000000..3ee22881f --- /dev/null +++ b/src/git_dirs.zig @@ -0,0 +1,265 @@ +//! In-process `git rev-parse --path-format=absolute --git-dir --git-common-dir`. +//! +//! Boot asks "which checkout am I in" three times (auto-isolate, startup +//! claim, presence). Each git spawn costs ~11 ms, so the common layouts are +//! answered here from the filesystem: a `.git` directory (main checkout) or a +//! `.git` file holding `gitdir:` (linked worktree, separate git dir). The walk +//! mirrors git's discovery (setup.c): `.git` first at each level, stop at a +//! filesystem boundary, validate HEAD / objects / refs, and print paths the +//! way `strbuf_realpath` does. +//! +//! Anything git could answer differently is `.fallback`, and the caller asks +//! git: discovery env vars, bare repos or a cwd inside a git dir, symlinks +//! anywhere in a resolved path, foreign ownership (safe.directory), repository +//! format extensions or config includes, unreadable or oversized files, and +//! every platform but macOS and Linux. `.not_git` is returned only where git +//! itself fails discovery. + +const std = @import("std"); +const builtin = @import("builtin"); +const Io = std.Io; +const Allocator = std.mem.Allocator; + +pub const Dirs = struct { git_dir: []const u8, common_dir: []const u8 }; +pub const Result = union(enum) { not_git, found: Dirs, fallback }; + +const supported = builtin.os.tag == .macos or builtin.os.tag == .linux; + +/// Present at all (even empty) means git's discovery may differ from ours. +pub const discovery_env = [_][:0]const u8{ + "GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", + "GIT_CEILING_DIRECTORIES", "GIT_DISCOVERY_ACROSS_FILESYSTEM", "GIT_OBJECT_DIRECTORY", + "GIT_IMPLICIT_WORK_TREE", "GIT_TEST_ASSUME_DIFFERENT_OWNER", +}; + +/// Tests only: pretend a discovery env var is (true) or is not (false) set. +pub var env_override_for_test: ?bool = null; + +pub fn envOverride() bool { + if (env_override_for_test) |v| return v; + if (comptime !supported) return true; + for (discovery_env) |name| if (std.c.getenv(name.ptr) != null) return true; + return false; +} + +const Kind = enum { dir, file, link, other }; +const Info = struct { kind: Kind, dev: u64, uid: u32 }; +const Lstat = union(enum) { ok: Info, missing, failed }; + +fn kindOf(mode: u32) Kind { + return switch (mode & 0o170000) { + 0o040000 => .dir, + 0o100000 => .file, + 0o120000 => .link, + else => .other, + }; +} + +fn lstat(path: []const u8) Lstat { + if (comptime !supported) return .failed; + var zbuf: [std.fs.max_path_bytes + 1]u8 = undefined; + if (path.len == 0 or path.len >= zbuf.len) return .failed; + @memcpy(zbuf[0..path.len], path); + zbuf[path.len] = 0; + const z: [*:0]const u8 = zbuf[0..path.len :0]; + if (builtin.os.tag == .linux) { + const linux = std.os.linux; + var sx: linux.Statx = undefined; + const rc = linux.statx(linux.AT.FDCWD, z, linux.AT.SYMLINK_NOFOLLOW, .{ .TYPE = true, .MODE = true, .UID = true }, &sx); + return switch (linux.errno(rc)) { + .SUCCESS => .{ .ok = .{ .kind = kindOf(sx.mode), .dev = (@as(u64, sx.dev_major) << 32) | sx.dev_minor, .uid = sx.uid } }, + .NOENT, .NOTDIR => .missing, + else => .failed, + }; + } + var st: std.c.Stat = undefined; + if (std.c.fstatat(std.c.AT.FDCWD, z, &st, std.c.AT.SYMLINK_NOFOLLOW) != 0) { + return switch (std.posix.errno(@as(c_int, -1))) { + .NOENT, .NOTDIR => .missing, + else => .failed, + }; + } + return .{ .ok = .{ .kind = kindOf(@intCast(st.mode)), .dev = @bitCast(@as(i64, st.dev)), .uid = @intCast(st.uid) } }; +} + +/// git's `access(path, X_OK)` on objects/ and refs/. +fn searchable(path: []const u8) bool { + var zbuf: [std.fs.max_path_bytes + 1]u8 = undefined; + if (path.len == 0 or path.len >= zbuf.len) return false; + @memcpy(zbuf[0..path.len], path); + zbuf[path.len] = 0; + return std.c.access(zbuf[0..path.len :0], std.c.X_OK) == 0; +} + +fn child(arena: Allocator, dir: []const u8, name: []const u8) ?[]const u8 { + if (std.mem.eql(u8, dir, "/")) return std.fmt.allocPrint(arena, "/{s}", .{name}) catch null; + return std.fmt.allocPrint(arena, "{s}/{s}", .{ dir, name }) catch null; +} + +fn readSmall(io: Io, arena: Allocator, path: []const u8, cap: usize) ?[]const u8 { + return Io.Dir.cwd().readFileAlloc(io, path, arena, .limited(cap)) catch null; +} + +fn trimNewlines(s: []const u8) []const u8 { + return std.mem.trimEnd(u8, s, "\r\n"); +} + +/// git's `strbuf_realpath` for a path with no symlink in it: `.` and `..` +/// fold lexically. Null when any component is a symlink or does not exist, +/// because then the OS realpath and git's may disagree. +pub fn plainRealPath(arena: Allocator, abs: []const u8) ?[]const u8 { + if (!std.fs.path.isAbsolute(abs)) return null; + var out: std.ArrayList(u8) = .empty; + var it = std.mem.tokenizeScalar(u8, abs, '/'); + while (it.next()) |comp| { + if (std.mem.eql(u8, comp, ".")) continue; + if (std.mem.eql(u8, comp, "..")) { + out.shrinkRetainingCapacity(std.mem.lastIndexOfScalar(u8, out.items, '/') orelse 0); + continue; + } + out.append(arena, '/') catch return null; + out.appendSlice(arena, comp) catch return null; + switch (lstat(out.items)) { + .ok => |i| if (i.kind == .link) return null, + else => return null, + } + } + if (out.items.len == 0) return "/"; + return out.items; +} + +/// setup.c `validate_headref`: a regular HEAD naming `refs/…` or an oid. +fn validHead(io: Io, arena: Allocator, git_dir: []const u8) bool { + const p = child(arena, git_dir, "HEAD") orelse return false; + switch (lstat(p)) { + .ok => |i| if (i.kind != .file) return false, + else => return false, + } + const bytes = readSmall(io, arena, p, 255) orelse return false; + if (std.mem.startsWith(u8, bytes, "ref:")) { + const rest = std.mem.trimStart(u8, bytes[4..], " \t\n\r\x0b\x0c"); + return std.mem.startsWith(u8, rest, "refs/"); + } + if (bytes.len < 40) return false; + for (bytes[0..40]) |c| if (!std.ascii.isHex(c)) return false; + return true; +} + +/// Repository format git is sure to accept: version 0/1, no extensions, no +/// includes that could add them. Anything else is git's call. +pub fn plainFormat(io: Io, arena: Allocator, common: []const u8) bool { + const p = child(arena, common, "config") orelse return false; + const raw = readSmall(io, arena, p, 64 * 1024) orelse return false; + const text = std.ascii.allocLowerString(arena, raw) catch return false; + if (std.mem.indexOf(u8, text, "[extensions") != null) return false; + if (std.mem.indexOf(u8, text, "[include") != null) return false; + const key = "repositoryformatversion"; + var at: usize = 0; + while (std.mem.indexOfPos(u8, text, at, key)) |i| { + at = i + key.len; + var v = std.mem.trimStart(u8, text[at..], " \t"); + if (v.len == 0 or v[0] != '=') return false; + v = v[1..]; + const end = std.mem.indexOfAny(u8, v, "\n;#") orelse v.len; + const val = std.mem.trim(u8, v[0..end], " \t\r"); + if (!std.mem.eql(u8, val, "0") and !std.mem.eql(u8, val, "1")) return false; + } + return true; +} + +/// setup.c `is_git_directory` plus the format check, for `git_dir` whose +/// shared parts live in `common`. +fn validRepo(io: Io, arena: Allocator, git_dir: []const u8, common: []const u8) bool { + if (!validHead(io, arena, git_dir)) return false; + const objects = child(arena, common, "objects") orelse return false; + const refs = child(arena, common, "refs") orelse return false; + return searchable(objects) and searchable(refs) and plainFormat(io, arena, common); +} + +fn owned(path: []const u8, euid: u32) bool { + return switch (lstat(path)) { + .ok => |i| i.uid == euid, + else => false, + }; +} + +fn mainCheckout(io: Io, arena: Allocator, top: []const u8, dot_git: []const u8, euid: u32) Result { + if (!owned(top, euid) or !owned(dot_git, euid)) return .fallback; + const commondir = child(arena, dot_git, "commondir") orelse return .fallback; + if (lstat(commondir) != .missing) return .fallback; + if (!validRepo(io, arena, dot_git, dot_git)) return .fallback; + return .{ .found = .{ .git_dir = dot_git, .common_dir = dot_git } }; +} + +fn gitFile(io: Io, arena: Allocator, top: []const u8, dot_git: []const u8, euid: u32) Result { + const bytes = readSmall(io, arena, dot_git, 4096) orelse return .fallback; + const prefix = "gitdir: "; + if (!std.mem.startsWith(u8, bytes, prefix)) return .fallback; + const raw = trimNewlines(bytes[prefix.len..]); + if (raw.len == 0 or std.mem.indexOfScalar(u8, raw, 0) != null) return .fallback; + const joined = if (std.fs.path.isAbsolute(raw)) raw else (child(arena, top, raw) orelse return .fallback); + const git_dir = plainRealPath(arena, joined) orelse return .fallback; + if (!owned(dot_git, euid) or !owned(top, euid) or !owned(git_dir, euid)) return .fallback; + const commondir = child(arena, git_dir, "commondir") orelse return .fallback; + const common = switch (lstat(commondir)) { + .missing => git_dir, + .ok => |i| blk: { + if (i.kind != .file) return .fallback; + const data = trimNewlines(readSmall(io, arena, commondir, 4096) orelse return .fallback); + if (data.len == 0) return .fallback; + const path = if (std.fs.path.isAbsolute(data)) data else (child(arena, git_dir, data) orelse return .fallback); + break :blk plainRealPath(arena, path) orelse return .fallback; + }, + .failed => return .fallback, + }; + if (!validRepo(io, arena, git_dir, common)) return .fallback; + return .{ .found = .{ .git_dir = git_dir, .common_dir = common } }; +} + +/// Resolve from `start`, an absolute physical directory path (getcwd or a +/// realpath). The strings in `.found` are what git prints, byte for byte. +pub fn resolve(io: Io, arena: Allocator, start: []const u8) Result { + if (comptime !supported) return .fallback; + if (envOverride()) return .fallback; + if (!std.fs.path.isAbsolute(start)) return .fallback; + const euid: u32 = @intCast(std.c.geteuid()); + if (euid == 0) return .fallback; // git consults SUDO_UID for root + const first = switch (lstat(start)) { + .ok => |i| i, + else => return .fallback, + }; + if (first.kind != .dir) return .fallback; + var cur: []const u8 = if (start.len > 1) std.mem.trimEnd(u8, start, "/") else start; + if (cur.len == 0) cur = "/"; + while (true) { + const dot_git = child(arena, cur, ".git") orelse return .fallback; + switch (lstat(dot_git)) { + .ok => |i| return switch (i.kind) { + .dir => mainCheckout(io, arena, cur, dot_git, euid), + .file => gitFile(io, arena, cur, dot_git, euid), + else => .fallback, + }, + .missing => {}, + .failed => return .fallback, + } + // A directory holding HEAD may be a bare repo or the inside of a git + // dir — git's `is_git_directory` branch. Never guess. + const head = child(arena, cur, "HEAD") orelse return .fallback; + switch (lstat(head)) { + .missing => {}, + else => return .fallback, + } + if (std.mem.eql(u8, cur, "/")) return .not_git; + const parent = std.fs.path.dirname(cur) orelse return .not_git; + switch (lstat(parent)) { + // git stops at a filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM unset). + .ok => |i| if (i.dev != first.dev) return .not_git, + else => return .fallback, + } + cur = parent; + } +} + +test { + _ = @import("git_dirs_tests.zig"); +} diff --git a/src/git_dirs_tests.zig b/src/git_dirs_tests.zig new file mode 100644 index 000000000..64284084b --- /dev/null +++ b/src/git_dirs_tests.zig @@ -0,0 +1,213 @@ +//! git_dirs.zig against real `git rev-parse`, and the identity memo. +//! Fixtures live under the system temp dir, outside any checkout, so the +//! "no repository" row really is one. Every row skips when git is missing. + +const std = @import("std"); +const builtin = @import("builtin"); +const Io = std.Io; +const Allocator = std.mem.Allocator; + +const git_dirs = @import("git_dirs.zig"); +const lease = @import("worktree_lease.zig"); +const process_runner = @import("process_runner.zig"); + +const supported = builtin.os.tag == .macos or builtin.os.tag == .linux; + +/// Run git with hermetic init/commit settings; true on exit 0. +pub fn git(io: Io, argv: []const []const u8) bool { + const gpa = std.testing.allocator; + var full: std.ArrayList([]const u8) = .empty; + defer full.deinit(gpa); + full.appendSlice(gpa, &.{ "git", "-c", "init.defaultObjectFormat=sha1", "-c", "init.defaultRefFormat=files", "-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null" }) catch return false; + full.appendSlice(gpa, argv) catch return false; + const r = process_runner.runCapped(gpa, io, full.items, 1 << 16, 1 << 16, 30_000) catch return false; + defer { + gpa.free(r.stdout); + gpa.free(r.stderr); + } + return process_runner.ranOk(r); +} + +/// A fresh physical scratch dir outside any checkout, or null without git. +pub fn scratch(io: Io, arena: Allocator) ?[]const u8 { + if (!supported or !git(io, &.{"--version"})) return null; + const tmp = if (std.c.getenv("TMPDIR")) |t| std.mem.span(t) else "/tmp"; + var raw: [6]u8 = undefined; + io.random(&raw); + const dir = std.fmt.allocPrint(arena, "{s}/graff-git-dirs-{x}", .{ std.mem.trimEnd(u8, tmp, "/"), raw }) catch return null; + Io.Dir.cwd().createDirPath(io, dir) catch return null; + return Io.Dir.cwd().realPathFileAlloc(io, dir, arena) catch null; +} + +pub fn repoWithCommit(io: Io, arena: Allocator, root: []const u8) !void { + _ = arena; + if (!git(io, &.{ "init", "-q", "-b", "main", root })) return error.GitInitFailed; + if (!git(io, &.{ "-C", root, "commit", "-q", "--allow-empty", "-m", "i" })) return error.GitCommitFailed; +} + +fn gitAnswer(io: Io, arena: Allocator, path: []const u8) ?git_dirs.Dirs { + const gpa = std.testing.allocator; + const r = process_runner.runCapped(gpa, io, &.{ "git", "-C", path, "rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir" }, 8192, 8192, 15_000) catch return null; + defer { + gpa.free(r.stdout); + gpa.free(r.stderr); + } + if (!process_runner.ranOk(r)) return null; + var lines = std.mem.splitScalar(u8, std.mem.trimEnd(u8, r.stdout, "\r\n"), '\n'); + const g = arena.dupe(u8, lines.next() orelse return null) catch return null; + const c = arena.dupe(u8, lines.next() orelse return null) catch return null; + return .{ .git_dir = g, .common_dir = c }; +} + +const Want = enum { not_git, found, fallback }; + +/// The in-process answer is the expected kind, equals git's when it answers, +/// and the full identity equals the one built from git's output. +fn expectRow(io: Io, arena: Allocator, path: []const u8, want: Want) !void { + const truth = gitAnswer(io, arena, path); + const got = git_dirs.resolve(io, arena, path); + errdefer std.debug.print("git_dirs row {s}: got {t}, git {?s}\n", .{ path, got, if (truth) |t| t.git_dir else null }); + try std.testing.expectEqual(want, @as(Want, switch (got) { + .not_git => .not_git, + .found => .found, + .fallback => .fallback, + })); + switch (got) { + .not_git => try std.testing.expect(truth == null), + .found => |d| { + const t = truth orelse return error.GitSawNoRepo; + try std.testing.expectEqualStrings(t.git_dir, d.git_dir); + try std.testing.expectEqualStrings(t.common_dir, d.common_dir); + }, + .fallback => {}, + } + const t = truth orelse git_dirs.Dirs{ .git_dir = "", .common_dir = "" }; + const want_id = lease.canonicalIdentity(t.git_dir, t.common_dir, path); + const id = lease.identityAt(std.testing.allocator, io, arena, path); + try std.testing.expectEqualStrings(want_id.id, id.id); + try std.testing.expectEqual(want_id.kind, id.kind); +} + +test "git_dirs: fixture matrix matches git rev-parse byte for byte" { + if (git_dirs.envOverride()) return error.SkipZigTest; + const io = std.testing.io; + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + const base = scratch(io, a) orelse return error.SkipZigTest; + defer Io.Dir.cwd().deleteTree(io, base) catch {}; + const p = struct { + fn at(ar: Allocator, b: []const u8, rel: []const u8) []const u8 { + return std.fmt.allocPrint(ar, "{s}/{s}", .{ b, rel }) catch unreachable; + } + }; + + // No .git anywhere up to the filesystem boundary. + try Io.Dir.cwd().createDirPath(io, p.at(a, base, "plain/deep")); + try expectRow(io, a, p.at(a, base, "plain/deep"), .not_git); + + // `.git` directory: the checkout root and a nested subdirectory. + const repo = p.at(a, base, "repo"); + try repoWithCommit(io, a, repo); + try Io.Dir.cwd().createDirPath(io, p.at(a, base, "repo/sub/deep")); + try expectRow(io, a, repo, .found); + try expectRow(io, a, p.at(a, base, "repo/sub/deep"), .found); + + // `.git` file with gitdir: + commondir (linked worktree), and inside it. + try std.testing.expect(git(io, &.{ "-C", repo, "worktree", "add", "-q", "--detach", p.at(a, base, "wt") })); + try Io.Dir.cwd().createDirPath(io, p.at(a, base, "wt/sub")); + try expectRow(io, a, p.at(a, base, "wt"), .found); + try expectRow(io, a, p.at(a, base, "wt/sub"), .found); + + // `.git` file without commondir (separate git dir). + try std.testing.expect(git(io, &.{ "init", "-q", "--separate-git-dir", p.at(a, base, "sep.git"), p.at(a, base, "sepwt") })); + try expectRow(io, a, p.at(a, base, "sepwt"), .found); + + // Bare repository and a cwd inside a git dir: git's call. + try std.testing.expect(git(io, &.{ "init", "-q", "--bare", p.at(a, base, "bare.git") })); + try expectRow(io, a, p.at(a, base, "bare.git"), .fallback); + try expectRow(io, a, p.at(a, base, "repo/.git/refs"), .fallback); + + // A gitdir reached through a symlink: realpath semantics could differ. + try Io.Dir.cwd().symLink(io, p.at(a, base, "sep.git"), p.at(a, base, "sep-link"), .{ .is_directory = true }); + try Io.Dir.cwd().createDirPath(io, p.at(a, base, "symwt")); + const gitfile = try std.fmt.allocPrint(a, "gitdir: {s}\n", .{p.at(a, base, "sep-link")}); + try Io.Dir.cwd().writeFile(io, .{ .sub_path = p.at(a, base, "symwt/.git"), .data = gitfile }); + try expectRow(io, a, p.at(a, base, "symwt"), .fallback); + + // Repository format extensions: older git may refuse the repo. + const ext = p.at(a, base, "ext"); + try std.testing.expect(git(io, &.{ "init", "-q", ext })); + try std.testing.expect(git(io, &.{ "-C", ext, "config", "extensions.worktreeConfig", "true" })); + try expectRow(io, a, ext, .fallback); + + // A discovery env var (GIT_DIR and friends) always defers to git. + git_dirs.env_override_for_test = true; + defer git_dirs.env_override_for_test = null; + try std.testing.expect(git_dirs.resolve(io, a, repo) == .fallback); +} + +var stub_calls: u32 = 0; +fn stubRevParse(gpa: Allocator, io: Io, arena: Allocator, dir: ?[]const u8) lease.RevParse { + _ = .{ gpa, io, arena, dir }; + stub_calls += 1; + return .{ .git_dir = "/stub/.git", .common_dir = "/stub/.git" }; +} + +/// Swap the identity spawn for a counter; restore with the returned value. +pub fn countSpawns() lease.RevParseFn { + const saved = lease.rev_parse_hook; + lease.rev_parse_hook = &stubRevParse; + stub_calls = 0; + return saved; +} + +pub fn spawnCalls() u32 { + return stub_calls; +} + +test "currentIdentity: one git answer per cwd, forgotten on chdir and invalidate" { + if (!supported) return error.SkipZigTest; + const io = std.testing.io; + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + const gpa = std.testing.allocator; + git_dirs.env_override_for_test = true; // force the git path + defer git_dirs.env_override_for_test = null; + const saved = countSpawns(); + defer lease.rev_parse_hook = saved; + lease.invalidateIdentity(); + defer lease.invalidateIdentity(); + + const first = lease.currentIdentity(gpa, io, a); + try std.testing.expectEqualStrings("/stub/.git", first.id); + try std.testing.expectEqual(lease.Identity.Kind.main_checkout, first.kind); + try std.testing.expectEqual(@as(u32, 1), stub_calls); + _ = lease.currentIdentity(gpa, io, a); + try std.testing.expectEqual(@as(u32, 1), stub_calls); + lease.invalidateIdentity(); + _ = lease.currentIdentity(gpa, io, a); + try std.testing.expectEqual(@as(u32, 2), stub_calls); + _ = lease.freshIdentity(gpa, io, a); + try std.testing.expectEqual(@as(u32, 3), stub_calls); + + // A different cwd is a different key; coming back is another miss. + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var orig = try Io.Dir.cwd().openDir(io, ".", .{}); + defer orig.close(io); + if (std.posix.system.fchdir(tmp.dir.handle) != 0) return error.ChdirFailed; + { + defer _ = std.posix.system.fchdir(orig.handle); + _ = lease.currentIdentity(gpa, io, a); + _ = lease.currentIdentity(gpa, io, a); + try std.testing.expectEqual(@as(u32, 4), stub_calls); + } + _ = lease.currentIdentity(gpa, io, a); + try std.testing.expectEqual(@as(u32, 5), stub_calls); +} + +test { + _ = @import("worktree_reap_gate.zig"); +} diff --git a/src/main.zig b/src/main.zig index 4b428fab2..318534784 100644 --- a/src/main.zig +++ b/src/main.zig @@ -595,5 +595,5 @@ test { // ── Unit tests (`zig build test`): pull in tests from imported modu _ = .{ @import("acp_replay.zig"), @import("python_version.zig"), @import("jev_auto.zig"), @import("background_wait.zig"), @import("gateway_compact_tests.zig") }; _ = @import("read_image.zig"); _ = @import("image_orient.zig"); - _ = .{ @import("mcp_interop_tests.zig"), @import("mcp_inbound.zig"), @import("mcp_names.zig"), @import("workspace_history.zig"), @import("workspace_tree_snap.zig"), @import("file_worktree.zig"), @import("review_deadline.zig"), @import("issue_cmd.zig"), @import("shell_tool.zig"), @import("acp_ask.zig"), @import("side_steer.zig"), @import("task_workspace_tests.zig"), @import("workspace_prepare_tests.zig"), @import("worktree_reap.zig") }; + _ = .{ @import("mcp_interop_tests.zig"), @import("mcp_inbound.zig"), @import("mcp_names.zig"), @import("workspace_history.zig"), @import("workspace_tree_snap.zig"), @import("file_worktree.zig"), @import("review_deadline.zig"), @import("issue_cmd.zig"), @import("shell_tool.zig"), @import("acp_ask.zig"), @import("side_steer.zig"), @import("task_workspace_tests.zig"), @import("workspace_prepare_tests.zig"), @import("worktree_reap.zig"), @import("git_dirs_tests.zig") }; } diff --git a/src/presence.zig b/src/presence.zig index 8b98b9fe6..e97ad9543 100644 --- a/src/presence.zig +++ b/src/presence.zig @@ -212,7 +212,7 @@ pub fn announce(io: Io, gpa: Allocator, arena: Allocator, home: []const u8, sess pub fn rebind(io: Io, gpa: Allocator, arena: Allocator) void { if (builtin.is_test or g_own_name == null) return; - const identity = worktree_lease.currentIdentity(gpa, io, arena); + const identity = worktree_lease.freshIdentity(gpa, io, arena); if (identity.id.len == 0 or std.mem.eql(u8, identity.id, g_identity)) return; if (g_identity.len > 0) gpa.free(g_identity); g_identity = gpa.dupe(u8, identity.id) catch return; diff --git a/src/session_start.zig b/src/session_start.zig index e3e98f53a..43a032b34 100644 --- a/src/session_start.zig +++ b/src/session_start.zig @@ -126,7 +126,7 @@ pub fn setupWorktreeAndBanner( // agents don't collide on files. Creates .graff/worktrees/ on branch // worktree- from the remote base and enters it; reuses it if it exists. const task_workspace = @import("task_workspace.zig"); - _ = @import("worktree_reap.zig").orphans(gpa, io, arena, "."); + _ = @import("worktree_reap.zig").orphansAtBoot(gpa, io, arena, "."); if (flags.worktree_flag) |wt| { // POSIX-only: the chdir below goes through libc's `chdir`, which Windows // builds don't link. -w is a parallel-agent dev workflow (mac/linux); on diff --git a/src/task_workspace.zig b/src/task_workspace.zig index bb587b0eb..f8c6cc076 100644 --- a/src/task_workspace.zig +++ b/src/task_workspace.zig @@ -399,6 +399,7 @@ pub fn enter(io: Io, arena: Allocator, wt: Workspace) !void { if (builtin.os.tag == .windows) return error.Unsupported; const z = try arena.dupeSentinel(u8, wt.path, 0); if (std.posix.system.chdir(z.ptr) != 0) return error.ChdirFailed; + lease.invalidateIdentity(); // Session arena outlives the process; GPA dupes leaked at shutdown. main_mod.g_cwd_display = try arena.dupe(u8, wt.path); main_mod.g_worktree_branch = try arena.dupe(u8, wt.branch); @@ -409,7 +410,10 @@ pub fn enter(io: Io, arena: Allocator, wt: Workspace) !void { /// A failed `git worktree add` is an isolation failure — do not stay on the claimed tree. pub fn maybeAutoIsolate(gpa: Allocator, io: Io, arena: Allocator, home: []const u8, already_isolated: bool, lean: bool) AutoIsolate { if (builtin.os.tag == .windows) return .skip; - const is_git = isGitAt(gpa, io, "."); + // Lean one-shots (ADR 0024) and isolated sessions never isolate: decide before any git work. + if (already_isolated or lean) return .skip; + const identity = lease.currentIdentity(gpa, io, arena); + const is_git = identity.id.len > 0 and identity.kind != .not_git; const eligible = shouldAutoIsolate(.{ .already_isolated = already_isolated, .lean = lean, .is_git = is_git, .claimed = true, .windows = false }); // #1200: a live peer, or one that started with us and has not announced yet. if (!eligible or !(checkoutClaimed(gpa, io, arena, home) or startupContended(gpa, io, arena, home))) return .skip; diff --git a/src/task_workspace_tests.zig b/src/task_workspace_tests.zig index c0244c393..1631508a1 100644 --- a/src/task_workspace_tests.zig +++ b/src/task_workspace_tests.zig @@ -298,3 +298,39 @@ fn git(io: Io, a: Allocator, argv: []const []const u8) !void { } if (!ranOk(r)) return error.TestUnexpectedResult; } + +test "maybeAutoIsolate: lean, -w and non-git runs spawn no git (F3)" { + if (builtin.os.tag == .windows) return error.SkipZigTest; + const git_dirs = @import("git_dirs.zig"); + if (git_dirs.envOverride()) return error.SkipZigTest; + const lease = @import("worktree_lease.zig"); + const fx = @import("git_dirs_tests.zig"); + const io = std.testing.io; + const a = std.testing.allocator; + var arena_state = std.heap.ArenaAllocator.init(a); + defer arena_state.deinit(); + const ar = arena_state.allocator(); + const saved = fx.countSpawns(); + defer lease.rev_parse_hook = saved; + lease.invalidateIdentity(); + defer lease.invalidateIdentity(); + const spawns = lease.rev_parse_spawns.load(.monotonic); + + // Lean one-shots and already-isolated sessions decide before any git work. + try std.testing.expect(ws.maybeAutoIsolate(a, io, ar, "/nonexistent-home", false, true) == .skip); + try std.testing.expect(ws.maybeAutoIsolate(a, io, ar, "/nonexistent-home", true, false) == .skip); + // An interactive run in a plain folder learns "not git" in-process. + const base = fx.scratch(io, ar) orelse return error.SkipZigTest; + defer Io.Dir.cwd().deleteTree(io, base) catch {}; + var plain = try Io.Dir.cwd().openDir(io, base, .{}); + defer plain.close(io); + var orig = try Io.Dir.cwd().openDir(io, ".", .{}); + defer orig.close(io); + if (std.posix.system.fchdir(plain.handle) != 0) return error.ChdirFailed; + { + defer _ = std.posix.system.fchdir(orig.handle); + try std.testing.expect(ws.maybeAutoIsolate(a, io, ar, "/nonexistent-home", false, false) == .skip); + } + try std.testing.expectEqual(@as(u32, 0), fx.spawnCalls()); + try std.testing.expectEqual(spawns, lease.rev_parse_spawns.load(.monotonic)); +} diff --git a/src/worktree_lease.zig b/src/worktree_lease.zig index 7efc8f696..ff46f79ca 100644 --- a/src/worktree_lease.zig +++ b/src/worktree_lease.zig @@ -25,6 +25,7 @@ const Allocator = std.mem.Allocator; const process_runner = @import("process_runner.zig"); const proc_identity = @import("proc_identity.zig"); +const git_dirs = @import("git_dirs.zig"); const runCapped = process_runner.runCapped; const ranOk = process_runner.ranOk; @@ -198,29 +199,120 @@ fn revParseAt(gpa: Allocator, io: Io, arena: Allocator, dir: []const u8, flag: [ return arena.dupe(u8, std.mem.trim(u8, r.stdout, " \t\r\n")) catch ""; } -/// Live canonical identity for this process's working directory. +/// git's two answers; both empty when git found no repository (or failed). +pub const RevParse = struct { git_dir: []const u8 = "", common_dir: []const u8 = "" }; +pub const RevParseFn = *const fn (gpa: Allocator, io: Io, arena: Allocator, dir: ?[]const u8) RevParse; + +/// The one place identity spawns git. Swappable so tests can count or forbid +/// it; `rev_parse_spawns` counts real spawns. +pub var rev_parse_hook: RevParseFn = &spawnRevParse; +pub var rev_parse_spawns: std.atomic.Value(u32) = .init(0); + +/// ONE `git rev-parse --git-dir --git-common-dir`. A path with a newline in it +/// cannot be split from the pair, so it takes the old one-flag-per-spawn path. +fn spawnRevParse(gpa: Allocator, io: Io, arena: Allocator, dir: ?[]const u8) RevParse { + _ = rev_parse_spawns.fetchAdd(1, .monotonic); + const tail = [_][]const u8{ "rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir" }; + const r = (if (dir) |d| runCapped(gpa, io, &([_][]const u8{ "git", "-C", d } ++ tail), 8192, 8192, 15_000) else runCapped(gpa, io, &([_][]const u8{"git"} ++ tail), 8192, 8192, 15_000)) catch return .{}; + defer { + gpa.free(r.stdout); + gpa.free(r.stderr); + } + if (!ranOk(r)) return .{}; + var lines = std.mem.splitScalar(u8, std.mem.trimEnd(u8, r.stdout, "\r\n"), '\n'); + const first = lines.next() orelse ""; + const second = lines.next(); + if (second == null or lines.next() != null) { + _ = rev_parse_spawns.fetchAdd(2, .monotonic); + if (dir) |d| return .{ .git_dir = revParseAt(gpa, io, arena, d, "--git-dir"), .common_dir = revParseAt(gpa, io, arena, d, "--git-common-dir") }; + return .{ .git_dir = revParse(gpa, io, arena, "--git-dir"), .common_dir = revParse(gpa, io, arena, "--git-common-dir") }; + } + const trim = " \t\r\n"; + return .{ + .git_dir = arena.dupe(u8, std.mem.trim(u8, first, trim)) catch "", + .common_dir = arena.dupe(u8, std.mem.trim(u8, second.?, trim)) catch "", + }; +} + +// Memo of git's answer for one cwd (getcwd, what git itself sees). In-process +// answers are cheap and always recomputed; only the spawn is cached. +var memo_lock: std.atomic.Value(bool) = .init(false); +var memo_key: [4096]u8 = undefined; +var memo_key_len: usize = 0; +var memo_id: [4096]u8 = undefined; +var memo_id_len: usize = 0; +var memo_kind: Identity.Kind = .not_git; + +fn memoLock() void { + while (memo_lock.cmpxchgWeak(false, true, .acquire, .monotonic) != null) std.atomic.spinLoopHint(); +} + +/// Forget the memoized identity. Every chdir of the process calls this. +pub fn invalidateIdentity() void { + memoLock(); + defer memo_lock.store(false, .release); + memo_key_len = 0; +} + +fn memoGet(arena: Allocator, key: []const u8) ?Identity { + memoLock(); + defer memo_lock.store(false, .release); + if (memo_key_len == 0 or !std.mem.eql(u8, memo_key[0..memo_key_len], key)) return null; + return .{ .id = arena.dupe(u8, memo_id[0..memo_id_len]) catch return null, .kind = memo_kind }; +} + +fn memoPut(key: []const u8, id: Identity) void { + if (key.len == 0 or key.len > memo_key.len or id.id.len > memo_id.len) return; + memoLock(); + defer memo_lock.store(false, .release); + @memcpy(memo_key[0..key.len], key); + memo_key_len = key.len; + @memcpy(memo_id[0..id.id.len], id.id); + memo_id_len = id.id.len; + memo_kind = id.kind; +} + +/// Live canonical identity for this process's working directory. Zero git +/// spawns for ordinary layouts (git_dirs.zig); otherwise one, memoized. pub fn currentIdentity(gpa: Allocator, io: Io, arena: Allocator) Identity { - const git_dir = revParse(gpa, io, arena, "--git-dir"); - const common = gitCommonDir(gpa, io, arena); + var here_buf: [std.fs.max_path_bytes]u8 = undefined; + const here: []const u8 = if (std.process.currentPath(io, &here_buf)) |n| here_buf[0..n] else |_| ""; + if (here.len > 0) if (memoGet(arena, here)) |hit| return hit; var buf: [4096]u8 = undefined; const cwd = blk: { const n = Io.Dir.cwd().realPathFile(io, ".", &buf) catch break :blk ""; break :blk arena.dupe(u8, buf[0..n]) catch ""; }; - return canonicalIdentity(git_dir, common, cwd); + if (here.len > 0) switch (git_dirs.resolve(io, arena, here)) { + .not_git => return canonicalIdentity("", "", cwd), + .found => |d| return canonicalIdentity(d.git_dir, d.common_dir, cwd), + .fallback => {}, + }; + const rp = rev_parse_hook(gpa, io, arena, null); + const id = canonicalIdentity(rp.git_dir, rp.common_dir, cwd); + memoPut(here, id); + return id; +} + +/// `currentIdentity` after the cwd may have changed under the same path. +pub fn freshIdentity(gpa: Allocator, io: Io, arena: Allocator) Identity { + invalidateIdentity(); + return currentIdentity(gpa, io, arena); } /// Identity of an explicit path (`git -C` / `cd` target), not the caller cwd. pub fn identityAt(gpa: Allocator, io: Io, arena: Allocator, path: []const u8) Identity { if (path.len == 0) return .{}; - const git_dir = revParseAt(gpa, io, arena, path, "--git-dir"); - const common = revParseAt(gpa, io, arena, path, "--git-common-dir"); var buf: [4096]u8 = undefined; - const cwd = blk: { - const n = Io.Dir.cwd().realPathFile(io, path, &buf) catch break :blk path; - break :blk arena.dupe(u8, buf[0..n]) catch path; + const real: ?[]const u8 = if (Io.Dir.cwd().realPathFile(io, path, &buf)) |n| (arena.dupe(u8, buf[0..n]) catch null) else |_| null; + const cwd = real orelse path; + if (real) |start| switch (git_dirs.resolve(io, arena, start)) { + .not_git => return canonicalIdentity("", "", cwd), + .found => |d| return canonicalIdentity(d.git_dir, d.common_dir, cwd), + .fallback => {}, }; - return canonicalIdentity(git_dir, common, cwd); + const rp = rev_parse_hook(gpa, io, arena, path); + return canonicalIdentity(rp.git_dir, rp.common_dir, cwd); } pub fn identityLine(arena: Allocator, id: Identity) []const u8 { diff --git a/src/worktree_reap.zig b/src/worktree_reap.zig index b27ec733f..d9093317c 100644 --- a/src/worktree_reap.zig +++ b/src/worktree_reap.zig @@ -90,6 +90,16 @@ pub fn orphans(gpa: Allocator, io: Io, arena: Allocator, cwd: []const u8) usize return removed; } +/// Boot-time `orphans`: same result, but no git spawn when an in-process look +/// at the worktree registrations proves there is nothing to prune or reap. +pub fn orphansAtBoot(gpa: Allocator, io: Io, arena: Allocator, cwd: []const u8) usize { + const dir = if (cwd.len > 0) cwd else "."; + const wt_dir = if (std.mem.eql(u8, dir, ".")) ".graff/worktrees" else (std.fs.path.join(arena, &.{ dir, ".graff", "worktrees" }) catch return 0); + if ((Io.Dir.cwd().statFile(io, wt_dir, .{}) catch null) == null) return 0; + if (!@import("worktree_reap_gate.zig").mayHaveOrphans(io, arena, dir)) return 0; + return orphans(gpa, io, arena, dir); +} + /// After an agent task: drop stale registrations and dead session checkouts. /// Named task workspaces stay; this does not archive them. pub fn reapAfterTask(gpa: Allocator, io: Io) void { diff --git a/src/worktree_reap_gate.zig b/src/worktree_reap_gate.zig new file mode 100644 index 000000000..e535d08db --- /dev/null +++ b/src/worktree_reap_gate.zig @@ -0,0 +1,118 @@ +//! In-process precheck for `worktree_reap.orphans` at boot (#1118). +//! +//! `orphans` costs two git spawns (`worktree prune` + `worktree list`, plus +//! more per candidate) and changes something only when git would prune a +//! registration or a listed tree belongs to a dead auto-isolate session. +//! `mayHaveOrphans` answers that from `/worktrees` without a spawn. +//! It may say true when nothing would happen; it must never say false when +//! `orphans` would remove or prune anything. Every unclear case says true. + +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; + +const git_dirs = @import("git_dirs.zig"); +const proc_identity = @import("proc_identity.zig"); +const reap = @import("worktree_reap.zig"); + +fn join(arena: Allocator, a: []const u8, b: []const u8) ?[]const u8 { + return std.fs.path.join(arena, &.{ a, b }) catch null; +} + +fn exists(io: Io, path: []const u8) bool { + return (Io.Dir.cwd().statFile(io, path, .{}) catch null) != null; +} + +/// HEAD of one worktree git dir names `worktree-session--…` and that pid +/// is gone. Unreadable HEAD is treated as possibly dead. +fn deadSessionHead(io: Io, arena: Allocator, git_dir: []const u8) bool { + const p = join(arena, git_dir, "HEAD") orelse return true; + const bytes = Io.Dir.cwd().readFileAlloc(io, p, arena, .limited(4096)) catch return true; + if (!std.mem.startsWith(u8, bytes, "ref:")) return false; // detached: no branch line + const ref = std.mem.trim(u8, bytes[4..], " \t\r\n"); + const pid = reap.sessionPid(ref) orelse return false; + return proc_identity.probe(io, pid) == .gone; +} + +/// git's `should_prune_worktree` and `prune_dups`, conservatively. +fn prunable(io: Io, arena: Allocator, base: []const u8, common: []const u8, seen: *std.StringHashMapUnmanaged(void)) bool { + const locked = join(arena, base, "locked") orelse return true; + if (exists(io, locked)) return false; // git never prunes a locked tree + const gitdir = join(arena, base, "gitdir") orelse return true; + const bytes = Io.Dir.cwd().readFileAlloc(io, gitdir, arena, .limited(4096)) catch return true; + const path = std.mem.trimEnd(u8, bytes, "\r\n"); + if (path.len == 0 or !std.fs.path.isAbsolute(path)) return true; + if (!exists(io, path)) return true; + if (std.mem.eql(u8, path, common)) return true; + const gop = seen.getOrPut(arena, path) catch return true; + return gop.found_existing; +} + +/// False only when `orphans(cwd)` would provably change nothing. +pub fn mayHaveOrphans(io: Io, arena: Allocator, cwd: []const u8) bool { + const dir = if (cwd.len > 0) cwd else "."; + var buf: [std.fs.max_path_bytes]u8 = undefined; + const start: []const u8 = if (std.mem.eql(u8, dir, ".")) + (if (std.process.currentPath(io, &buf)) |n| buf[0..n] else |_| return true) + else + (if (Io.Dir.cwd().realPathFile(io, dir, &buf)) |n| buf[0..n] else |_| return true); + const common = switch (git_dirs.resolve(io, arena, start)) { + .found => |d| d.common_dir, + .not_git => return false, // both git calls fail outside a repo + .fallback => return true, + }; + if (deadSessionHead(io, arena, common)) return true; // the main worktree's own entry + const root = join(arena, common, "worktrees") orelse return true; + var wt_dir = Io.Dir.cwd().openDir(io, root, .{ .iterate = true }) catch |err| return err != error.FileNotFound; + defer wt_dir.close(io); + var seen: std.StringHashMapUnmanaged(void) = .empty; + var any = false; + var it = wt_dir.iterate(); + while (it.next(io) catch return true) |e| { + any = true; + if (e.kind != .directory) return true; + const base = join(arena, root, e.name) orelse return true; + if (prunable(io, arena, base, common, &seen)) return true; + if (deadSessionHead(io, arena, base)) return true; + } + return !any; // prune also removes an empty worktrees/ directory +} + +test "mayHaveOrphans: only stale registrations or dead session trees need git" { + const builtin = @import("builtin"); + if (builtin.os.tag != .macos and builtin.os.tag != .linux) return error.SkipZigTest; + if (git_dirs.envOverride()) return error.SkipZigTest; + const a = std.testing.allocator; + const io = std.testing.io; + const fx = @import("git_dirs_tests.zig"); + var arena_state = std.heap.ArenaAllocator.init(a); + defer arena_state.deinit(); + const ar = arena_state.allocator(); + const base = fx.scratch(io, ar) orelse return error.SkipZigTest; + defer Io.Dir.cwd().deleteTree(io, base) catch {}; + const root = try std.fmt.allocPrint(ar, "{s}/repo", .{base}); + try fx.repoWithCommit(io, ar, root); + const ws = @import("task_workspace.zig"); + + // No linked trees: nothing to prune, nothing to reap. + try std.testing.expect(!mayHaveOrphans(io, ar, root)); + const named = try ws.create(a, io, ar, .{ .slug = "task-keep", .cwd = root }); + _ = try ws.create(a, io, ar, .{ .slug = "session-1-deadbeef", .cwd = root }); // pid 1 lives + try std.testing.expect(!mayHaveOrphans(io, ar, root)); + try std.testing.expectEqual(@as(usize, 0), reap.orphans(a, io, ar, root)); + + // A dead session's clean tree is reapable. + _ = try ws.create(a, io, ar, .{ .slug = "session-0-deadbeef", .cwd = root }); + try std.testing.expect(mayHaveOrphans(io, ar, root)); + try std.testing.expect(reap.orphans(a, io, ar, root) >= 1); + try std.testing.expect(!mayHaveOrphans(io, ar, root)); + + // A checkout deleted out of band leaves a registration git would prune. + try Io.Dir.cwd().deleteTree(io, named.path); + try std.testing.expect(mayHaveOrphans(io, ar, root)); + _ = reap.orphans(a, io, ar, root); + try std.testing.expect(!mayHaveOrphans(io, ar, root)); + + // Outside any repo there is nothing for git to do. + try std.testing.expect(!mayHaveOrphans(io, ar, base)); +} From a07c9f2fb7459a2abe639b72a96717158bc7a77a Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:29:46 +0800 Subject: [PATCH 03/21] cli: serve the REPL update notice from a cached release tag The banner did a blocking GitHub latest-release GET on every interactive launch (~300 ms). It now reads ~/.graff/update-check.json and refreshes a missing or >24h-old cache on a detached thread with a private Io that neither boot nor exit waits on. graff update and /version still fetch live and write the tag they see to the same cache. Co-Authored-By: Codegraff --- ...AFT-update-check-stale-while-revalidate.md | 40 ++ src/cli.zig | 18 +- src/main.zig | 2 +- src/update_check_cache.zig | 343 ++++++++++++++++++ src/version_status.zig | 6 +- 5 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 docs/adr/DRAFT-update-check-stale-while-revalidate.md create mode 100644 src/update_check_cache.zig diff --git a/docs/adr/DRAFT-update-check-stale-while-revalidate.md b/docs/adr/DRAFT-update-check-stale-while-revalidate.md new file mode 100644 index 000000000..0d430095a --- /dev/null +++ b/docs/adr/DRAFT-update-check-stale-while-revalidate.md @@ -0,0 +1,40 @@ +# NNNN. The REPL update notice reads a cache; the network never holds the banner + +Status: accepted 2026-11-12 + +## Context + +The interactive REPL banner called `version_status.checkLatest`, a blocking +GET of GitHub's latest-release endpoint, on every launch. There was no cache +and no time bound beyond the HTTP client's own. With the check on, the banner +phase took about 320–350 ms on a normal network (25–28 ms when the request +failed fast), the largest REPL startup cost. `-p`, ACP and `--json` already +skipped it; `GRAFF_NO_UPDATE_CHECK=1` disabled it. + +## Decision + +The banner notice is stale-while-revalidate (`src/update_check_cache.zig`), +the same shape as model catalogs ([0306](0306-model-catalogs-never-hold-startup.md)): + +- The banner reads `~/.graff/update-check.json` + (`{"v":1,"tag":"","checked_ms":}`) and renders the + notice from it. Boot makes no network call. +- When that file is missing, unreadable, a different `v`, holds an + unparseable tag, or is 24 h old or more (or dated in the future), one + detached thread refetches the tag and atomically rewrites the file. It has + a private `Io` and allocator, nothing waits for it, and exit never joins + it; a worker cut off by exit leaves the old file in place. +- `GRAFF_NO_UPDATE_CHECK` reads no file and starts no thread. +- The notice wording and the version comparison (`version_status.compare`) + are unchanged; a cached tag not newer than the running build shows nothing. +- `graff update` and `/version` still fetch live, and write the tag they see + to the same file. + +## Consequences + +A new release shows its notice one launch late, and on the first launch +ever there is no notice. If every session exits before its refresh lands, +the cache stays stale and each launch retries once in the background — no +more requests than the old per-launch check. Revisit if the notice must be +same-launch accurate; that needs a bounded wait, not a return to the +blocking fetch. diff --git a/src/cli.zig b/src/cli.zig index 8ca413f2e..53caf7cd6 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -16,14 +16,20 @@ const harness_version = root.harness_version; const version_status = @import("version_status.zig"); const update_service = @import("update_service.zig"); const update_target = @import("update_target.zig"); +const update_check_cache = @import("update_check_cache.zig"); -/// Dim REPL/TUI line when GitHub has a newer release. Null if current, newer, -/// offline, or GRAFF_NO_UPDATE_CHECK is set. +/// Dim REPL/TUI line when the cached latest release is newer. Null if current, +/// newer, no cache yet, or GRAFF_NO_UPDATE_CHECK is set. Never touches the +/// network: a missing or stale cache refreshes on a detached thread. pub fn updateAvailableLine(io: Io, gpa: Allocator, arena: Allocator, skip: bool) ?[]const u8 { - if (skip) return null; - const check = version_status.checkLatest(io, gpa, arena, harness_version); - if (check.order != .lt) return null; - return std.fmt.allocPrint(arena, "graff v{s} is available (you have {s}) — graff update", .{ check.latest.?, check.running }) catch null; + _ = gpa; + return update_check_cache.banner(io, arena, .{ + .home = @import("credential_store.zig").g_home, + .running = harness_version, + .skip = skip, + .now_ms = @import("util.zig").unixMs(io), + .fetch = version_status.fetchLatestReleaseTag, + }).line; } /// `graff update [--force|--check]` — bring the installed binary up to the diff --git a/src/main.zig b/src/main.zig index 318534784..a618251d8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -124,7 +124,7 @@ test { _ = @import("obs_cost_test.zig"); _ = @import("external_tui.zig"); _ = provider_mod; - _ = .{ @import("named_work.zig"), @import("task_intent.zig") }; + _ = .{ @import("named_work.zig"), @import("task_intent.zig"), @import("update_check_cache.zig") }; } const prompts = @import("prompts.zig"); const schema = @import("schema.zig"); diff --git a/src/update_check_cache.zig b/src/update_check_cache.zig new file mode 100644 index 000000000..6326cfae8 --- /dev/null +++ b/src/update_check_cache.zig @@ -0,0 +1,343 @@ +//! The REPL banner's update notice reads a disk cache; it never fetches. +//! +//! The banner used to GET GitHub's latest-release endpoint on every launch, +//! which held the interactive banner for ~300 ms on a normal network. Now it +//! renders the notice from `~/.graff/update-check.json` (the last release tag +//! seen and when) and, when that file is missing, unreadable, or older than +//! `ttl_ms`, refreshes it on a detached thread. The refresh follows +//! `catalog_bounded.zig`: a private Io and allocator, nothing waits for it, +//! exit never joins it, and its only output is the cache file, written +//! atomically. A new release therefore shows its notice one launch late. +//! +//! The explicit commands (`graff update`, `/version`) still fetch live and +//! write the tag they saw here (`remember`). + +const std = @import("std"); +const builtin = @import("builtin"); +const Io = std.Io; +const Allocator = std.mem.Allocator; + +const version_status = @import("version_status.zig"); +const credential_store = @import("credential_store.zig"); +const util = @import("util.zig"); + +/// A cached tag younger than this is used without a refresh. +pub const ttl_ms: i64 = 24 * 60 * 60 * 1000; +/// Bumped when the file shape changes; any other value reads as missing. +pub const format_version: i64 = 1; +pub const rel_path = ".graff/update-check.json"; +const max_bytes = 4096; +const max_tag = 64; + +/// Returns the latest release tag, or null. Production passes +/// `version_status.fetchLatestReleaseTag`; tests pass a fake. +pub const Fetch = *const fn (io: Io, gpa: Allocator, arena: Allocator, running_version: []const u8) ?[]const u8; + +pub const Entry = struct { tag: []const u8, checked_ms: i64 }; + +pub const Opts = struct { + home: []const u8, + running: []const u8, + skip: bool, + now_ms: i64, + fetch: Fetch, +}; + +pub const Outcome = struct { + line: ?[]const u8 = null, + /// A detached refresh was started. + refresh: bool = false, +}; + +/// The banner notice from the cache alone. Starts a detached refresh when the +/// cache is missing, corrupt, foreign, or stale. `skip` (GRAFF_NO_UPDATE_CHECK) +/// reads nothing and starts nothing. +pub fn banner(io: Io, arena: Allocator, opts: Opts) Outcome { + if (opts.skip) return .{}; + const entry = read(io, arena, opts.home); + var out: Outcome = .{}; + if (entry == null or !isFresh(entry.?, opts.now_ms)) out.refresh = detach(opts.home, opts.running, opts.fetch); + if (entry) |e| out.line = noticeLine(arena, opts.running, e.tag); + return out; +} + +/// The notice for `tag`, or null when the running build is not older. +pub fn noticeLine(arena: Allocator, running: []const u8, tag: []const u8) ?[]const u8 { + const check = version_status.compare(running, tag); + if (check.order != .lt) return null; + return std.fmt.allocPrint(arena, "graff v{s} is available (you have {s}) — graff update", .{ check.latest.?, check.running }) catch null; +} + +pub fn isFresh(entry: Entry, now_ms: i64) bool { + const age = now_ms - entry.checked_ms; + return age >= 0 and age < ttl_ms; +} + +/// Record a tag a live check just saw, under the startup-pinned home. +pub fn remember(io: Io, tag: ?[]const u8) void { + const t = tag orelse return; + _ = store(io, credential_store.g_home, t, util.unixMs(io)); +} + +pub fn read(io: Io, arena: Allocator, home: []const u8) ?Entry { + if (home.len == 0) return null; + const path = std.fmt.allocPrint(arena, "{s}/" ++ rel_path, .{home}) catch return null; + const data = Io.Dir.cwd().readFileAlloc(io, path, arena, .limited(max_bytes)) catch return null; + return parse(arena, data); +} + +fn parse(arena: Allocator, data: []const u8) ?Entry { + const Shape = struct { v: i64, tag: []const u8, checked_ms: i64 }; + const s = std.json.parseFromSliceLeaky(Shape, arena, data, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }) catch return null; + if (s.v != format_version or !validTag(s.tag)) return null; + return .{ .tag = s.tag, .checked_ms = s.checked_ms }; +} + +/// A parseable release tag made only of characters that need no JSON escape. +fn validTag(tag: []const u8) bool { + if (tag.len == 0 or tag.len > max_tag) return false; + for (tag) |c| switch (c) { + '0'...'9', 'a'...'z', 'A'...'Z', '.', '-', '+' => {}, + else => return false, + }; + return version_status.compare("0.0.0", tag).failure == .none; +} + +/// Atomically replace the cache with `tag` checked at `now_ms`. +pub fn store(io: Io, home: []const u8, tag: []const u8, now_ms: i64) bool { + if (home.len == 0 or !validTag(tag)) return false; + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const dir = std.fmt.bufPrint(&path_buf, "{s}/.graff", .{home}) catch return false; + Io.Dir.cwd().createDirPath(io, dir) catch return false; + var path_buf2: [std.fs.max_path_bytes]u8 = undefined; + const path = std.fmt.bufPrint(&path_buf2, "{s}/" ++ rel_path, .{home}) catch return false; + var body_buf: [256]u8 = undefined; + const body = std.fmt.bufPrint(&body_buf, "{{\"v\":{d},\"tag\":\"{s}\",\"checked_ms\":{d}}}\n", .{ format_version, tag, now_ms }) catch return false; + credential_store.replaceFile(io, Io.Dir.cwd(), path, body, .default_file) catch return false; + return true; +} + +/// Thread-safe and independent of the session: a detached task frees itself. +const alloc = std.heap.smp_allocator; + +/// Refreshes still running. Tests drain it (`waitIdle`) so no worker outlives +/// the test Io that started it; production never waits. +var live: std.atomic.Value(usize) = .init(0); + +const Task = struct { + home: []u8, + running: []u8, + fetch: Fetch, + + fn destroy(t: *Task) void { + alloc.free(t.home); + alloc.free(t.running); + alloc.destroy(t); + } + + fn run(t: *Task) void { + defer _ = live.fetchSub(1, .release); + defer t.destroy(); + var threaded = privateThreaded(); + defer threaded.deinit(); + const io = threaded.io(); + var arena_state = std.heap.ArenaAllocator.init(alloc); + defer arena_state.deinit(); + const tag = t.fetch(io, alloc, arena_state.allocator(), t.running) orelse return; + _ = store(io, t.home, tag, util.unixMs(io)); + } +}; + +/// Same as `catalog_bounded.privateThreaded`: the worker may outlive every +/// other Io, so it must not restore the SIGIO/SIGPIPE action on deinit. +fn privateThreaded() Io.Threaded { + var threaded: Io.Threaded = .init(alloc, .{}); + threaded.have_signal_handler = false; + return threaded; +} + +/// Start a refresh nobody waits for. False when it could not start. +fn detach(home: []const u8, running: []const u8, fetch: Fetch) bool { + if (builtin.single_threaded or home.len == 0) return false; + const t = alloc.create(Task) catch return false; + const home_d = alloc.dupe(u8, home) catch { + alloc.destroy(t); + return false; + }; + const running_d = alloc.dupe(u8, running) catch { + alloc.free(home_d); + alloc.destroy(t); + return false; + }; + t.* = .{ .home = home_d, .running = running_d, .fetch = fetch }; + _ = live.fetchAdd(1, .acquire); + const thread = std.Thread.spawn(.{}, Task.run, .{t}) catch { + _ = live.fetchSub(1, .release); + t.destroy(); + return false; + }; + thread.detach(); + return true; +} + +/// For tests: wait up to `budget` for every refresh to finish. +pub fn waitIdle(io: Io, budget: Io.Duration) bool { + const step: Io.Duration = .fromMilliseconds(5); + var left = budget.nanoseconds; + while (live.load(.acquire) != 0) { + if (left <= 0) return false; + io.sleep(step, .awake) catch return false; + left -= step.nanoseconds; + } + return true; +} + +var fake_calls: std.atomic.Value(usize) = .init(0); + +fn fakeFetch(io: Io, gpa: Allocator, arena: Allocator, running: []const u8) ?[]const u8 { + _ = .{ io, gpa, arena, running }; + _ = fake_calls.fetchAdd(1, .acq_rel); + return "v9.9.9"; +} + +const TestHome = struct { + tmp: std.testing.TmpDir, + home: [:0]u8, + + fn init() !TestHome { + var tmp = std.testing.tmpDir(.{}); + errdefer tmp.cleanup(); + const home = try tmp.dir.realPathFileAlloc(std.testing.io, ".", std.testing.allocator); + return .{ .tmp = tmp, .home = home }; + } + + fn deinit(h: *TestHome) void { + std.testing.allocator.free(h.home); + h.tmp.cleanup(); + } + + fn opts(h: *const TestHome, now_ms: i64) Opts { + return .{ .home = h.home, .running = "0.0.300", .skip = false, .now_ms = now_ms, .fetch = fakeFetch }; + } + + fn rawWrite(h: *TestHome, body: []const u8) !void { + try h.tmp.dir.createDirPath(std.testing.io, ".graff"); + try h.tmp.dir.writeFile(std.testing.io, .{ .sub_path = rel_path, .data = body }); + } +}; + +const day0: i64 = 1_700_000_000_000; +const notice_301 = "graff v0.0.301 is available (you have 0.0.300) — graff update"; + +fn expectCachedTag(h: *const TestHome, arena: Allocator, tag: []const u8) !void { + const e = read(std.testing.io, arena, h.home) orelse return error.TestExpectedCache; + try std.testing.expectEqualStrings(tag, e.tag); +} + +test "update check cache: fresh cache gives the notice with no refresh" { + var h = try TestHome.init(); + defer h.deinit(); + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + try std.testing.expect(store(std.testing.io, h.home, "v0.0.301", day0)); + const before = fake_calls.load(.acquire); + const out = banner(std.testing.io, arena, h.opts(day0 + ttl_ms - 1)); + try std.testing.expectEqualStrings(notice_301, out.line.?); + try std.testing.expect(!out.refresh); + try std.testing.expect(waitIdle(std.testing.io, .fromSeconds(10))); + try std.testing.expectEqual(before, fake_calls.load(.acquire)); +} + +test "update check cache: stale cache gives the notice and refreshes behind" { + if (builtin.single_threaded) return error.SkipZigTest; + var h = try TestHome.init(); + defer h.deinit(); + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + try std.testing.expect(store(std.testing.io, h.home, "v0.0.301", day0)); + const before = fake_calls.load(.acquire); + const out = banner(std.testing.io, arena, h.opts(day0 + ttl_ms)); + try std.testing.expectEqualStrings(notice_301, out.line.?); + try std.testing.expect(out.refresh); + try std.testing.expect(waitIdle(std.testing.io, .fromSeconds(10))); + try std.testing.expectEqual(before + 1, fake_calls.load(.acquire)); + try expectCachedTag(&h, arena, "v9.9.9"); +} + +test "update check cache: no cache gives no notice and refreshes behind" { + if (builtin.single_threaded) return error.SkipZigTest; + var h = try TestHome.init(); + defer h.deinit(); + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + const out = banner(std.testing.io, arena, h.opts(day0)); + try std.testing.expect(out.line == null); + try std.testing.expect(out.refresh); + try std.testing.expect(waitIdle(std.testing.io, .fromSeconds(10))); + try expectCachedTag(&h, arena, "v9.9.9"); +} + +test "update check cache: corrupt, foreign, and future caches read as missing or stale" { + if (builtin.single_threaded) return error.SkipZigTest; + var h = try TestHome.init(); + defer h.deinit(); + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + const bodies = [_][]const u8{ + "{not json", + "{\"v\":2,\"tag\":\"v0.0.301\",\"checked_ms\":1700000000000}", + "{\"v\":1,\"tag\":\"latest\",\"checked_ms\":1700000000000}", + "{\"v\":1,\"tag\":\"v0.0.301\\\"x\",\"checked_ms\":1700000000000}", + "{\"v\":1,\"checked_ms\":1700000000000}", + }; + for (bodies) |body| { + try h.rawWrite(body); + const out = banner(std.testing.io, arena, h.opts(day0)); + try std.testing.expect(out.line == null); + try std.testing.expect(out.refresh); + try std.testing.expect(waitIdle(std.testing.io, .fromSeconds(10))); + } + // A clock that went backwards: keep the notice, but refresh. + try std.testing.expect(store(std.testing.io, h.home, "v0.0.301", day0 + 1000)); + const out = banner(std.testing.io, arena, h.opts(day0)); + try std.testing.expectEqualStrings(notice_301, out.line.?); + try std.testing.expect(out.refresh); + try std.testing.expect(waitIdle(std.testing.io, .fromSeconds(10))); +} + +test "update check cache: GRAFF_NO_UPDATE_CHECK reads and starts nothing" { + var h = try TestHome.init(); + defer h.deinit(); + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + try std.testing.expect(store(std.testing.io, h.home, "v0.0.301", day0 - ttl_ms)); + var o = h.opts(day0); + o.skip = true; + const before = fake_calls.load(.acquire); + const out = banner(std.testing.io, arena_state.allocator(), o); + try std.testing.expect(out.line == null); + try std.testing.expect(!out.refresh); + try std.testing.expectEqual(@as(usize, 0), live.load(.acquire)); + try std.testing.expectEqual(before, fake_calls.load(.acquire)); +} + +test "update check cache: a cached tag at or below the running version shows nothing" { + var h = try TestHome.init(); + defer h.deinit(); + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + for ([_][]const u8{ "v0.0.300", "v0.0.299", "v0.0.300.0" }) |tag| { + try std.testing.expect(store(std.testing.io, h.home, tag, day0)); + const out = banner(std.testing.io, arena, h.opts(day0 + 1)); + try std.testing.expect(out.line == null); + try std.testing.expect(!out.refresh); + } + // Same comparison rules as the live check: a hotfix of the running base is newer. + try std.testing.expect(noticeLine(arena, "0.0.300", "v0.0.300.1") != null); + try std.testing.expect(noticeLine(arena, "0.0.301-2-gabc", "v0.0.301") == null); +} diff --git a/src/version_status.zig b/src/version_status.zig index 463c9b872..8ddea3c67 100644 --- a/src/version_status.zig +++ b/src/version_status.zig @@ -152,8 +152,12 @@ pub fn fetchLatestReleaseTag(io: Io, gpa: Allocator, arena: Allocator, running_v return if (tag == .string) tag.string else null; } +/// Live check for the explicit commands (`graff update`, `/version`); the tag +/// it sees also refreshes the banner's cache. pub fn checkLatest(io: Io, gpa: Allocator, arena: Allocator, running_version: []const u8) Check { - return compare(running_version, fetchLatestReleaseTag(io, gpa, arena, running_version)); + const tag = fetchLatestReleaseTag(io, gpa, arena, running_version); + @import("update_check_cache.zig").remember(io, tag); + return compare(running_version, tag); } /// Leak-free tag extract for tests and the update service: copies into `buf`. From 8d2456c2fd23b80127458918164bc7f5d6134582 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:53:25 +0800 Subject: [PATCH 04/21] startup: prewarm the -p companion handshake; teardown ends MCP retry pauses -p connected the companion (codedb-pro --mcp) on the boot thread after the plugin scan and MCP merge. boot_prewarm starts that connect after runSubcommand when the registry is known to be the empty one, and connectCompanion joins and merges it under the registry mutex. A cwd or settings mismatch discards it; a quick failure falls back to addServer. Registry.deinit joined a start sleeping 1.5s before its retry. The pause now waits on a per-registry event that deinit sets, so teardown returns promptly and still joins every task. Adds formal/BootPrewarm with negative controls and a draft ADR. Co-Authored-By: Codegraff --- docs/adr/DRAFT-boot-prewarm.md | 71 +++++ formal/BootPrewarm.cfg | 14 + formal/BootPrewarm.tla | 213 +++++++++++++++ formal/BootPrewarmNoCwdKey.cfg | 6 + formal/BootPrewarmNoJoin.cfg | 6 + formal/BootPrewarmNoRetryAbort.cfg | 6 + formal/BootPrewarmNoTeardownJoin.cfg | 6 + formal/boot-prewarm.md | 57 ++++ scripts/check-formal.py | 5 + src/boot_prewarm.zig | 380 +++++++++++++++++++++++++++ src/companion_boot.zig | 11 +- src/main.zig | 2 +- src/mcp.zig | 7 + src/mcp_boot.zig | 26 +- src/mcp_start.zig | 54 +++- src/startup.zig | 4 +- 16 files changed, 861 insertions(+), 7 deletions(-) create mode 100644 docs/adr/DRAFT-boot-prewarm.md create mode 100644 formal/BootPrewarm.cfg create mode 100644 formal/BootPrewarm.tla create mode 100644 formal/BootPrewarmNoCwdKey.cfg create mode 100644 formal/BootPrewarmNoJoin.cfg create mode 100644 formal/BootPrewarmNoRetryAbort.cfg create mode 100644 formal/BootPrewarmNoTeardownJoin.cfg create mode 100644 formal/boot-prewarm.md create mode 100644 src/boot_prewarm.zig diff --git a/docs/adr/DRAFT-boot-prewarm.md b/docs/adr/DRAFT-boot-prewarm.md new file mode 100644 index 000000000..705fb88ec --- /dev/null +++ b/docs/adr/DRAFT-boot-prewarm.md @@ -0,0 +1,71 @@ +# NNNN. A one-shot prewarms the companion handshake; teardown ends retry pauses + +Status: proposed (number assigned at merge) + +## Context + +`-p` connects the companion (`codedb-pro --mcp`) on the boot thread: +ADR 0230 defers it only for interactive and ACP sessions +(`companion_boot.deferCompanion`), so the first request's catalog already +holds the companion. Its spawn and handshake ran in series after +credentials, the plugin scan and the MCP merge. With no tool cache, a +hermetic one-shot with a stub companion sent its first request at a median +of 24.2 ms, against 14.0 ms with no companion on PATH. + +Teardown had the opposite problem. `Registry.deinit` joins every pending +start, and a start that failed transiently slept 1.5 s before its one retry, +with no way to wake it. A REPL whose companion failed at once was ready in +about 50 ms but took about 1.54 s to exit. + +## Decision + +- **Overlap, not defer.** A one-shot starts the companion connect on its own + task (`boot_prewarm.startCompanion`) right after `runSubcommand`, and the + boot thread joins it in `connectCompanion`, exactly where `addServer` ran. + The handshake is not deferred past the first request, so the first + request's tools, prompt and cache key are byte-identical to the serial + path. +- **Only when the outcome is known in advance.** The prewarm starts only + when the registry will be the empty one and this companion is the one + `connectCompanion` would try first: lean `-p`, no positionals, no `-w`, no + project `.mcp.json`, none of `GRAFF_MCP_PROBE`, `GRAFF_MCP_EAGER`, + `GRAFF_MCP_HANDSHAKE_SECS` or `GRAFF_MCP_PROBE_MS`, not opted out in + `.harness/settings.json`, and on `PATH`. Every other session keeps the + serial path. +- **Join at the consumer, or discard.** `take` adopts the result only if the + cwd, home and registry settings still match, awaits it, and merges it + under the registry mutex (ADR 0197). A mismatch, or a prewarm nobody + adopted, is cancelled, joined and torn down: in `connectCompanion`, and in + `Registry.deinitAtExit`. +- **A prewarm never loses the companion.** A quick failure falls back to the + inline `addServer`. A timeout is not repeated. +- **Teardown ends retry pauses.** Each registry owns a heap event, + `retry_abort`. `Registry.deinit` sets it before joining, and the pause in + `mcp_start` waits on it. A start waiting to retry stops waiting and + records its first failure. The registry still joins every task, so no + thread outlives `io` (#325). +- **Formal evidence.** `formal/BootPrewarm.tla` holds the invariants + `JoinedBeforeUse`, `NoStaleAdoption`, `NoUseAfterTeardown` and + `NoRetryAfterAbort`, each with a negative control. + +## Consequences + +- `-p` with an uncached companion reaches its first request about as fast as + `max(companion, rest of boot)` instead of their sum. A cached companion + (restored dormant) gains nothing and costs nothing. +- The companion's connect line on stderr can print earlier than other boot + diagnostics. +- When boot fails after the prewarm started (no credential, an error before + the registry exists), the companion has already been spawned. Process + exit closes its pipe, and `std.process.Init` joins the worker, which is + bounded by the handshake timeout. +- An interrupted retry is not attempted again in that process; the next + session starts the server again. +- The plugin scan is not prewarmed. `plugins.applyEnv` stores + `plugins.disabled` on the boot thread while a scan worker would read it in + `discoverFresh`. With the companion prewarmed, the scan already overlaps + the longer handshake. Revisit it if `applyEnv` stops storing an unchanged + value, or if the scan becomes the longest boot phase. +- Moving the one-shot companion to the background entirely, as ADR 0230 + does for interactive sessions, would make the first request's catalog + depend on timing. That stays an owner decision. diff --git a/formal/BootPrewarm.cfg b/formal/BootPrewarm.cfg new file mode 100644 index 000000000..ecbf19d23 --- /dev/null +++ b/formal/BootPrewarm.cfg @@ -0,0 +1,14 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = FALSE + BypassCwdKey = FALSE + BypassTeardownJoin = FALSE + BypassRetryAbort = FALSE +INVARIANTS TypeOK + JoinedBeforeUse + NoStaleAdoption + NoUseAfterTeardown + NoLeakAtExit + AtMostOneCompanion + NeverMissing + NoRetryAfterAbort +PROPERTIES TeardownFinishes diff --git a/formal/BootPrewarm.tla b/formal/BootPrewarm.tla new file mode 100644 index 000000000..afc78015c --- /dev/null +++ b/formal/BootPrewarm.tla @@ -0,0 +1,213 @@ +------------------------------ MODULE BootPrewarm ------------------------------ +EXTENDS Naturals, TLC + +\* A finite abstraction of the boot-thread companion prewarm (boot_prewarm.zig) +\* and of one registry start task's retry pause at teardown (mcp_start.zig). +\* The prewarm worker connects the companion before the registry exists; the +\* boot thread joins it at its one consumer (connectCompanion) or discards it. +CONSTANTS BypassJoin, BypassCwdKey, BypassTeardownJoin, BypassRetryAbort +ASSUME /\ BypassJoin \in BOOLEAN + /\ BypassCwdKey \in BOOLEAN + /\ BypassTeardownJoin \in BOOLEAN + /\ BypassRetryAbort \in BOOLEAN + +BootStates == {"pre", "consumer", "inline", "connected", "noConsumer", "joining", "exited"} +WorkerStates == {"none", "running", "done", "consumed"} +TaskStates == {"attempt", "pause", "retry", "done"} + +VARIABLES boot, worker, result, spawned, adopted, inlined, registered, + cwdMoved, cwdAtTake, usedEarly, ioAlive, task, abort, retriedAfterAbort + +vars == <> + +prewarm == <> +taskVars == <> + +\* The gate either declined (no slot) or started the worker after +\* runSubcommand; the registry's own start task is in its first attempt. +Init == + /\ boot = "pre" + /\ worker \in {"none", "running"} + /\ result = "none" + /\ spawned = (worker = "running") + /\ adopted = FALSE + /\ inlined = FALSE + /\ registered = 0 + /\ cwdMoved = FALSE + /\ cwdAtTake = FALSE + /\ usedEarly = FALSE + /\ ioAlive = TRUE + /\ task = "attempt" + /\ abort = FALSE + /\ retriedAfterAbort = FALSE + +\* connectTask: one attempt on a task-local registry. A failure leaves no +\* process; success leaves a prewarm-owned server. +WorkerFinish == + /\ worker = "running" /\ ioAlive + /\ \E r \in {"ok", "fail"}: + /\ result' = r + /\ spawned' = (r = "ok" /\ ~adopted) + /\ worker' = "done" + /\ UNCHANGED <> /\ UNCHANGED taskVars + +\* -w or auto-isolation between prewarm start and the consumer. +MoveCwd == + /\ boot = "pre" /\ ~cwdMoved + /\ cwdMoved' = TRUE + /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + +ReachConsumer == + /\ boot = "pre" + /\ boot' = "consumer" + /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + +\* A path that exits after the registry exists without reaching the +\* consumer (--selftest-spinner); deinitAtExit discards the slot. +SkipConsumer == + /\ boot = "pre" + /\ boot' = "noConsumer" + /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + +KeyMatches == ~cwdMoved \/ BypassCwdKey + +\* take(): await, then mergeOutcomes under the registry mutex. +TakeAdopt == + /\ boot = "consumer" /\ KeyMatches + /\ \/ worker = "done" /\ result = "ok" + \/ BypassJoin /\ worker = "running" + /\ usedEarly' = (worker = "running") + /\ cwdAtTake' = cwdMoved + /\ worker' = IF worker = "done" THEN "consumed" ELSE worker + /\ adopted' = TRUE + /\ spawned' = FALSE + /\ registered' = registered + 1 + /\ boot' = "connected" + /\ UNCHANGED <> /\ UNCHANGED taskVars + +\* A failed prewarm falls back to addServer on the boot thread. +TakeFailed == + /\ boot = "consumer" /\ KeyMatches + /\ worker = "done" /\ result = "fail" + /\ worker' = "consumed" + /\ boot' = "inline" + /\ UNCHANGED <> /\ UNCHANGED taskVars + +\* A key mismatch discards: Future.cancel is a joined operation, then the +\* server is torn down. +TakeMismatch == + /\ boot = "consumer" /\ ~KeyMatches + /\ worker \in {"running", "done"} + /\ worker' = "consumed" + /\ spawned' = FALSE + /\ boot' = "inline" + /\ UNCHANGED <> /\ UNCHANGED taskVars + +TakeNone == + /\ boot = "consumer" /\ worker = "none" + /\ boot' = "inline" + /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + +InlineConnect == + /\ boot = "inline" + /\ \E ok \in BOOLEAN: registered' = registered + (IF ok THEN 1 ELSE 0) + /\ inlined' = TRUE + /\ boot' = "connected" + /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + +\* deinitAtExit: discard any unadopted prewarm (cancel + join), then +\* Registry.deinit sets retry_abort and joins its start tasks. +Teardown == + /\ boot \in {"connected", "noConsumer"} + /\ IF BypassTeardownJoin + THEN UNCHANGED <> + ELSE /\ worker' = IF worker \in {"running", "done"} THEN "consumed" ELSE worker + /\ spawned' = FALSE + /\ abort' = TRUE + /\ boot' = "joining" + /\ UNCHANGED <> /\ UNCHANGED taskVars + +\* main returns; std.process.Init tears the Io down after its join. +IoDown == + /\ boot = "joining" + /\ BypassTeardownJoin \/ task = "done" + /\ ioAlive' = FALSE + /\ boot' = "exited" + /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + +\* The registry's start task: one attempt, a transient failure pauses, and +\* the pause either times out into the one retry or ends on retry_abort. +AttemptEnds == + /\ task = "attempt" /\ ioAlive + /\ task' \in {"pause", "done"} + /\ UNCHANGED retriedAfterAbort + /\ UNCHANGED <> /\ UNCHANGED prewarm + +PauseTimesOut == + /\ task = "pause" /\ ioAlive + /\ ~abort \/ BypassRetryAbort + /\ task' = "retry" + /\ retriedAfterAbort' = abort + /\ UNCHANGED <> /\ UNCHANGED prewarm + +PauseAborted == + /\ task = "pause" /\ abort /\ ~BypassRetryAbort + /\ task' = "done" + /\ UNCHANGED retriedAfterAbort + /\ UNCHANGED <> /\ UNCHANGED prewarm + +RetryEnds == + /\ task = "retry" /\ ioAlive + /\ task' = "done" + /\ UNCHANGED retriedAfterAbort + /\ UNCHANGED <> /\ UNCHANGED prewarm + +Next == + \/ WorkerFinish \/ MoveCwd \/ ReachConsumer \/ SkipConsumer + \/ TakeAdopt \/ TakeFailed \/ TakeMismatch \/ TakeNone \/ InlineConnect + \/ Teardown \/ IoDown + \/ AttemptEnds \/ PauseTimesOut \/ PauseAborted \/ RetryEnds + +Spec == Init /\ [][Next]_vars + /\ WF_vars(WorkerFinish) /\ WF_vars(ReachConsumer) + /\ WF_vars(TakeAdopt \/ TakeFailed \/ TakeMismatch \/ TakeNone \/ InlineConnect) + /\ WF_vars(Teardown) /\ WF_vars(IoDown) + /\ WF_vars(AttemptEnds \/ PauseTimesOut \/ PauseAborted \/ RetryEnds) + +TypeOK == + /\ boot \in BootStates + /\ worker \in WorkerStates + /\ result \in {"none", "ok", "fail"} + /\ registered \in 0..2 + /\ task \in TaskStates + /\ {spawned, adopted, inlined, cwdMoved, cwdAtTake, usedEarly, ioAlive, abort, retriedAfterAbort} \subseteq BOOLEAN + +\* The boot thread never uses the prewarm outcome before the worker ended. +JoinedBeforeUse == ~usedEarly + +\* A prewarm recorded in another directory is never adopted. +NoStaleAdoption == adopted => ~cwdAtTake + +\* No thread still uses the Io after it is torn down. +NoUseAfterTeardown == ~ioAlive => (worker # "running" /\ task = "done") + +\* No prewarm-owned server outlives the registry teardown. +NoLeakAtExit == boot = "exited" => ~spawned + +\* The companion is registered once at most: adopted or inline, never both. +AtMostOneCompanion == registered <= 1 /\ ~(adopted /\ inlined) + +\* Reaching the consumer always ends adopted or with the inline connect tried. +NeverMissing == boot = "connected" => (adopted \/ inlined) + +\* Once teardown signals, no retry attempt starts. +NoRetryAfterAbort == ~retriedAfterAbort + +\* Under the fairness above, a started teardown finishes. +TeardownFinishes == (boot = "joining") ~> (boot = "exited") +============================================================================= diff --git a/formal/BootPrewarmNoCwdKey.cfg b/formal/BootPrewarmNoCwdKey.cfg new file mode 100644 index 000000000..acc23bcc7 --- /dev/null +++ b/formal/BootPrewarmNoCwdKey.cfg @@ -0,0 +1,6 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = FALSE + BypassCwdKey = TRUE + BypassTeardownJoin = FALSE + BypassRetryAbort = FALSE +INVARIANT NoStaleAdoption diff --git a/formal/BootPrewarmNoJoin.cfg b/formal/BootPrewarmNoJoin.cfg new file mode 100644 index 000000000..cc61365a4 --- /dev/null +++ b/formal/BootPrewarmNoJoin.cfg @@ -0,0 +1,6 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = TRUE + BypassCwdKey = FALSE + BypassTeardownJoin = FALSE + BypassRetryAbort = FALSE +INVARIANT JoinedBeforeUse diff --git a/formal/BootPrewarmNoRetryAbort.cfg b/formal/BootPrewarmNoRetryAbort.cfg new file mode 100644 index 000000000..8e8750244 --- /dev/null +++ b/formal/BootPrewarmNoRetryAbort.cfg @@ -0,0 +1,6 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = FALSE + BypassCwdKey = FALSE + BypassTeardownJoin = FALSE + BypassRetryAbort = TRUE +INVARIANT NoRetryAfterAbort diff --git a/formal/BootPrewarmNoTeardownJoin.cfg b/formal/BootPrewarmNoTeardownJoin.cfg new file mode 100644 index 000000000..9cd198d36 --- /dev/null +++ b/formal/BootPrewarmNoTeardownJoin.cfg @@ -0,0 +1,6 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = FALSE + BypassCwdKey = FALSE + BypassTeardownJoin = TRUE + BypassRetryAbort = FALSE +INVARIANT NoUseAfterTeardown diff --git a/formal/boot-prewarm.md b/formal/boot-prewarm.md new file mode 100644 index 000000000..f13d48cde --- /dev/null +++ b/formal/boot-prewarm.md @@ -0,0 +1,57 @@ +# Boot prewarm and retry abort + +`BootPrewarm.tla` is a finite model of two lifecycles that decide whether a +worker can still use the process `Io` when boot or teardown moves on: + +- the companion prewarm (`src/boot_prewarm.zig`, DRAFT-boot-prewarm ADR): a + worker connects `codedb-pro --mcp` before the registry exists, and the boot + thread joins it at its one consumer, `connectCompanion`; +- one registry start task's retry pause (`src/mcp_start.zig`): teardown must + not wait out the pause, and must still join the task (#325). + +| Model action | Implementation boundary | +| --- | --- | +| `Init` (worker `running` or `none`) | `startup.resolveKeysOrRunAcpPreAuth` calls `boot_prewarm.startCompanion` after `runSubcommand`; the gate (`wantsCompanion`, no project `.mcp.json`, opt-out, PATH) either declines or `start` launches `connectTask` with `io.concurrent`. | +| `WorkerFinish` | `connectTask`: one `startServer` on a task-local registry; a failure tears its server down before returning. | +| `MoveCwd` | `-w` or auto-isolation changing the cwd between start and consumer. | +| `TakeAdopt`, `TakeFailed`, `TakeMismatch`, `TakeNone` | `boot_prewarm.take`: `matches` (cwd key, home, probe and lazy settings), `Future.await`, then `mcp_boot.adoptStarted` under the registry mutex; a fast failure returns null so the caller runs `addServer`; a mismatch calls `discard`. | +| `InlineConnect` | `companion_boot.connectNow` falling back to `Registry.addServer`. | +| `SkipConsumer`, `Teardown` | `connectCompanion`'s deferred `discard`, and `Registry.deinitAtExit` calling `discard` (cancel, join, tear down); `Registry.deinit` sets `retry_abort` before `joinPending`. | +| `IoDown` | `main` returns; `std.process.Init` deinitializes the threaded `Io` after joining its workers. | +| `AttemptEnds`, `PauseTimesOut`, `PauseAborted`, `RetryEnds` | `mcp_start.task` and `pauseBeforeRetry`: the pause is `Io.Event.waitTimeout` on the registry's heap-owned event. | + +The positive configuration checks, over every interleaving of these actions: +the outcome is never used before the worker ended (`JoinedBeforeUse`), a +prewarm recorded in another directory is never adopted (`NoStaleAdoption`), +no worker or start task is live once the `Io` is down (`NoUseAfterTeardown`), +no prewarm-owned server outlives teardown (`NoLeakAtExit`), the companion is +registered at most once (`AtMostOneCompanion`), the consumer always ends +adopted or with the inline connect tried (`NeverMissing`), and no retry starts +after teardown signalled (`NoRetryAfterAbort`). Under weak fairness of the +worker, consumer, teardown and task steps, a started teardown finishes +(`TeardownFinishes`). + +Run TLC from `formal/` with `-deadlock`, since exit is a terminal state. + +| Config | Deliberate mutation | Violated invariant | +| --- | --- | --- | +| `BootPrewarmNoJoin.cfg` | Adopt while the worker still runs | `JoinedBeforeUse` | +| `BootPrewarmNoCwdKey.cfg` | Adopt without comparing the cwd key | `NoStaleAdoption` | +| `BootPrewarmNoTeardownJoin.cfg` | Tear the `Io` down without joining the prewarm or the start task | `NoUseAfterTeardown` | +| `BootPrewarmNoRetryAbort.cfg` | Ignore `retry_abort` in the pause | `NoRetryAfterAbort` | + +These are mutation checks of the model, not historical regressions. The +retry pause has a runtime anchor that fails on the old revision: +`src/mcp_start.zig` `teardown with a pending transient retry returns in well +under the pause` (fails when `Registry.deinit` does not set the event). The +prewarm anchors are the `boot prewarm:` tests in `src/boot_prewarm.zig` +(byte-identical catalog against `addServer`, cwd-change discard, settings +mismatch, failed-prewarm fallback, discard of a silent server, and the +early-exit gate). + +Omitted: the handshake's own protocol states and timeouts, the content of the +catalog (covered by the byte-comparison test), the error-return paths that +leave `main` before the registry exists (there `std.process.Init` joins the +worker, bounded by the handshake timeout, and process exit closes the child's +pipe), and more than one start task. The model does not establish that the +Zig code refines it. diff --git a/scripts/check-formal.py b/scripts/check-formal.py index 08dacd4ce..1fe569ec8 100644 --- a/scripts/check-formal.py +++ b/scripts/check-formal.py @@ -30,6 +30,11 @@ ("Http2Leases.tla", "Http2Leases.cfg", None), ("Http2Leases.tla", "Http2LeasesRetainIdle.cfg", "IdleActiveDisjoint"), ("Http2Leases.tla", "Http2LeasesEarlyReuse.cfg", "OnlyEndedIdle"), + ("BootPrewarm.tla", "BootPrewarm.cfg", None), + ("BootPrewarm.tla", "BootPrewarmNoJoin.cfg", "JoinedBeforeUse"), + ("BootPrewarm.tla", "BootPrewarmNoCwdKey.cfg", "NoStaleAdoption"), + ("BootPrewarm.tla", "BootPrewarmNoTeardownJoin.cfg", "NoUseAfterTeardown"), + ("BootPrewarm.tla", "BootPrewarmNoRetryAbort.cfg", "NoRetryAfterAbort"), ) diff --git a/src/boot_prewarm.zig b/src/boot_prewarm.zig new file mode 100644 index 000000000..2e75a9c37 --- /dev/null +++ b/src/boot_prewarm.zig @@ -0,0 +1,380 @@ +//! Boot prewarm: run the companion handshake (`codedb-pro --mcp`) while the +//! rest of a one-shot boots, and join it where `connectCompanion` would have +//! called `addServer` (DRAFT-boot-prewarm ADR; formal/BootPrewarm.tla). +//! +//! `-p` connects the companion synchronously (ADR 0230 defers only +//! interactive and ACP sessions), so its spawn + handshake used to sit in +//! series with credentials, the plugin scan and the MCP merge. Only the case +//! where the result is known in advance prewarms: lean `-p` (not a +//! subcommand), no `-w`, no project `.mcp.json` and no registry env override. There the +//! registry is the empty one (`oneshotSkipsImportedMcp`), so the companion is +//! the only server that can connect and the adopted outcome fills exactly the +//! slot `addServer` would have. Every other case keeps the serial path. +//! +//! One slot, touched only by the boot thread: `start` after `runSubcommand`, +//! `take` in `connectCompanion` (await, then merge under the registry mutex, +//! ADR 0197), `discard` (cancel, join, tear down) when it is not adopted and +//! at registry teardown. A cwd or registry-setting mismatch discards; a quick +//! failure falls back to the inline `addServer`, a timeout does not repeat. +//! The worker owns copies of everything it reads: `std.process.Init` frees +//! `environ_map` before it joins worker threads. + +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; + +const args = @import("args.zig"); +const mcp = @import("mcp.zig"); +const mcp_boot = @import("mcp_boot.zig"); +const mcp_rpc = @import("mcp_rpc.zig"); +const mcp_teardown = @import("mcp_teardown.zig"); +const skills = @import("skills.zig"); + +/// What `addServer(name, command, args)` would start, plus the registry +/// settings `startServer` reads. `take` adopts only when the live registry +/// carries the same settings. +pub const Spec = struct { + name: []const u8, + command: []const u8, + args: []const []const u8 = &.{}, + home: []const u8 = "", + stdio_probe: bool = true, + lazy_stdio: bool = true, + show_diagnostics: bool = true, +}; + +const Joined = struct { outcome: mcp_boot.StartOutcome = .{}, err: ?anyerror = null }; + +const Slot = struct { + future: Io.Future(Joined), + owned: std.heap.ArenaAllocator, + spec: Spec, + cwd: [std.fs.max_path_bytes]u8 = undefined, + cwd_len: usize = 0, +}; + +var slot: ?Slot = null; + +/// Settings that only `Registry.init` reads. With any of them set the +/// prewarm could not match the registry, so it does not start. +const registry_env = [_][]const u8{ "GRAFF_MCP_PROBE", "GRAFF_MCP_EAGER", "GRAFF_MCP_HANDSHAKE_SECS", "GRAFF_MCP_PROBE_MS" }; + +/// The flag/env half of the gate: a lean one-shot that will build the empty +/// registry. Interactive and ACP sessions queue the companion instead. +pub fn wantsCompanion(flags: args.Flags, environ_map: anytype) bool { + if (flags.oneshot_prompt == null) return false; // the prompt is the positionals; subcommands have none + if (flags.is_subcommand or flags.worktree_flag != null) return false; + if (!(flags.effectiveLean() or environ_map.get("GRAFF_LEAN") != null)) return false; + for (registry_env) |k| if (environ_map.get(k) != null) return false; + return true; +} + +/// Called once boot knows this is a session (after `runSubcommand`). Picks +/// the companion `connectCompanion` would try first and starts it. +pub fn startCompanion(io: Io, gpa: Allocator, environ_map: anytype, flags: args.Flags) void { + if (!wantsCompanion(flags, environ_map)) return; + if (Io.Dir.cwd().access(io, ".mcp.json", .{})) |_| return else |_| {} // project servers: not the empty registry + const path_env = environ_map.get("PATH") orelse ""; + for (skills.companion_servers) |c| { + if (optedOut(io, c.server)) continue; + var buf: [1024]u8 = undefined; + if (@import("path_lookup.zig").resolve(io, path_env, c.bin, &buf) == null) continue; + start(io, gpa, .{ .name = c.server, .command = c.bin, .args = &.{"--mcp"}, .home = @import("keys_cli.zig").homeEnv(environ_map) orelse "" }); + return; + } +} + +/// `skills.loadSkillSettings` has not run yet at prewarm time; read the same +/// `{"skills": {"": false}}` opt-out it will apply. +fn optedOut(io: Io, server: []const u8) bool { + var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + const data = Io.Dir.cwd().readFileAlloc(io, @import("harness_policy.zig").settings_path, a, .limited(1 << 20)) catch return false; + return optedOutIn(a, data, server); +} + +fn optedOutIn(a: Allocator, data: []const u8, server: []const u8) bool { + const v = std.json.parseFromSliceLeaky(std.json.Value, a, data, .{ .allocate = .alloc_always }) catch return false; + if (v != .object) return false; + const section = v.object.get("skills") orelse return false; + if (section != .object) return false; + const entry = section.object.get(server) orelse return false; + return entry == .bool and !entry.bool; +} + +pub fn pending() bool { + return slot != null; +} + +/// Start one prewarm. A second call, an allocation failure or a pool with no +/// thread to spare leaves the slot empty: the consumer then runs inline. +pub fn start(io: Io, gpa: Allocator, spec: Spec) void { + if (slot != null) return; + var owned = std.heap.ArenaAllocator.init(gpa); + const copy = dupSpec(owned.allocator(), spec) catch { + owned.deinit(); + return; + }; + var s: Slot = .{ .future = undefined, .owned = owned, .spec = copy }; + s.cwd_len = cwdKey(io, &s.cwd); + // Never `io.async`: run inline it would serialize the boot again. + s.future = io.concurrent(connectTask, .{ gpa, io, copy }) catch { + s.owned.deinit(); + return; + }; + slot = s; +} + +fn dupSpec(a: Allocator, spec: Spec) Allocator.Error!Spec { + var copy = spec; + copy.name = try a.dupe(u8, spec.name); + copy.command = try a.dupe(u8, spec.command); + copy.home = try a.dupe(u8, spec.home); + const list = try a.alloc([]const u8, spec.args.len); + for (spec.args, list) |src, *dst| dst.* = try a.dupe(u8, src); + copy.args = list; + return copy; +} + +fn cwdKey(io: Io, buf: *[std.fs.max_path_bytes]u8) usize { + return Io.Dir.cwd().realPath(io, buf) catch 0; +} + +/// The worker: `addServer`'s connect on a task-local registry (the pattern +/// `mcp_start.attempt` uses), one attempt, no retry, no failure record. +fn connectTask(gpa: Allocator, io: Io, spec: Spec) Joined { + var arena_state = std.heap.ArenaAllocator.init(gpa); + const outcome = connectInto(gpa, io, spec, &arena_state) catch |err| { + arena_state.deinit(); + return .{ .err = err }; + }; + return .{ .outcome = outcome }; +} + +fn connectInto(gpa: Allocator, io: Io, spec: Spec, arena_state: *std.heap.ArenaAllocator) !mcp_boot.StartOutcome { + const a = arena_state.allocator(); + var tmp: mcp.Registry = .{ + .gpa = gpa, + .io = io, + .home = spec.home, + .arena_state = std.heap.ArenaAllocator.init(gpa), + .stdio_probe = spec.stdio_probe, + .lazy_stdio = spec.lazy_stdio, + .show_diagnostics = spec.show_diagnostics, + }; + defer tmp.arena_state.deinit(); + // Same object, same key order as `Registry.addServer`: the cache key and + // the relaunch config are derived from it. + var cfg: std.json.ObjectMap = .empty; + try cfg.put(a, "command", .{ .string = try a.dupe(u8, spec.command) }); + var argv = std.json.Array.init(a); + for (spec.args) |arg| try argv.append(.{ .string = try a.dupe(u8, arg) }); + try cfg.put(a, "args", .{ .array = argv }); + var servers: std.ArrayList(*mcp_rpc.Server) = .empty; + var tools: std.ArrayList(mcp.Tool) = .empty; + try tmp.startServer(a, &servers, &tools, try a.dupe(u8, spec.name), cfg); + return .{ .server = servers.items[0], .tools = tools.items, .arena_state = arena_state.* }; +} + +/// The consumer. Null: nothing usable for `name` (none started, or it was +/// discarded) — connect inline as before. Otherwise the joined result: the +/// server merged into `reg`, or the error `addServer` would have returned. +pub fn take(reg: *mcp.Registry, name: []const u8) ?anyerror!void { + const s = if (slot) |*s| s else return null; + if (!std.mem.eql(u8, s.spec.name, name)) return null; + if (!matches(reg, s)) { + discard(reg.io); + return null; + } + const joined = s.future.await(reg.io); + release(); + if (joined.err) |err| return if (repeatInline(err)) null else err; + if (!mcp_boot.adoptStarted(reg, joined.outcome)) return error.McpServerAlreadyConnected; + return {}; +} + +fn matches(reg: *const mcp.Registry, s: *const Slot) bool { + if (reg.stdio_probe != s.spec.stdio_probe or reg.lazy_stdio != s.spec.lazy_stdio) return false; + if (!std.mem.eql(u8, reg.home, s.spec.home)) return false; + var now: [std.fs.max_path_bytes]u8 = undefined; + const n = cwdKey(reg.io, &now); + return n > 0 and std.mem.eql(u8, now[0..n], s.cwd[0..s.cwd_len]); +} + +/// A prewarm that failed fast is retried on the boot thread, which is what +/// the serial path did at this point. A timeout already took the full +/// handshake budget; repeating it would double the worst case. +fn repeatInline(err: anyerror) bool { + return switch (err) { + error.McpHandshakeTimeout, error.McpRequestTimedOut, error.Canceled => false, + else => true, + }; +} + +/// Cancel, join and tear down a prewarm nobody adopted. No-op when the slot +/// is empty, so every exit path may call it. +pub fn discard(io: Io) void { + const s = if (slot) |*s| s else return; + var joined = s.future.cancel(io); + release(); + if (joined.outcome.server) |server| mcp_rpc.deinitServer(server, io, .init(io, mcp_teardown.teardown_grace)); + if (joined.outcome.arena_state) |*ta| ta.deinit(); +} + +fn release() void { + if (slot) |*s| s.owned.deinit(); + slot = null; +} + +const testing = std.testing; + +/// A legacy stdio MCP server in /bin/sh, as in mcp_restart.zig's fixture. +const fixture_script = + \\while read -r l; do + \\ id=$(printf '%s' "$l" | sed -n 's/^{"jsonrpc":"2.0","id":\([0-9]*\),.*/\1/p') + \\ [ -z "$id" ] && continue + \\ case "$l" in + \\ *'"method":"initialize"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"f","version":"1"}}}\n' "$id" ;; + \\ *'"method":"tools/list"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[{"name":"search","description":"Search","inputSchema":{"type":"object","properties":{"q":{"type":"string"}},"required":["q"]}},{"name":"read","description":"Read","inputSchema":{"type":"object"},"annotations":{"readOnlyHint":true}}]}}\n' "$id" ;; + \\ *) printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32601,"message":"no"}}\n' "$id" ;; + \\ esac + \\done +; + +fn fixtureSpec(name: []const u8, argv: []const []const u8) Spec { + return .{ .name = name, .command = "/bin/sh", .args = argv, .stdio_probe = false, .lazy_stdio = false, .show_diagnostics = false }; +} + +const fixture_argv = [_][]const u8{ "-c", fixture_script }; + +fn testRegistry() mcp.Registry { + var reg = mcp.Registry.empty(testing.allocator, testing.io); + reg.stdio_probe = false; + reg.lazy_stdio = false; + return reg; +} + +fn toolsJson(a: Allocator, reg: *mcp.Registry) ![]const u8 { + var out: std.Io.Writer.Allocating = .init(a); + for (reg.tools) |t| { + try out.writer.print("{s}|{s}|{s}|{d}|{any}|", .{ t.qualified_name, t.serverName(), t.description, t.server_index, t.read_only }); + try std.json.Stringify.value(t.input_schema, .{}, &out.writer); + try out.writer.writeByte('\n'); + } + for (reg.servers) |s| try out.writer.print("server {s} {s}\n", .{ s.name, s.protocol_version }); + return out.written(); +} + +test "boot prewarm: the adopted companion matches the inline addServer catalog byte for byte" { + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; // /bin/sh + var arena_state = std.heap.ArenaAllocator.init(testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + + var inline_reg = testRegistry(); + defer inline_reg.deinit(); + _ = try inline_reg.addServer("codedbpro", "/bin/sh", &.{ "-c", fixture_script }); + + var warm_reg = testRegistry(); + defer warm_reg.deinit(); + start(testing.io, testing.allocator, fixtureSpec("codedbpro", &fixture_argv)); + try testing.expect(pending()); + try testing.expect(take(&warm_reg, "other") == null); // another name leaves the slot alone + const joined = take(&warm_reg, "codedbpro") orelse return error.TestExpectedPrewarm; + try joined; + try testing.expect(!pending()); + try testing.expectEqual(@as(usize, 2), warm_reg.tools.len); + try testing.expectEqualStrings(try toolsJson(a, &inline_reg), try toolsJson(a, &warm_reg)); +} + +test "boot prewarm: a cwd change between start and consumer discards it and the inline path runs" { + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; + var reg = testRegistry(); + defer reg.deinit(); + start(testing.io, testing.allocator, fixtureSpec("codedbpro", &fixture_argv)); + try testing.expect(pending()); + slot.?.cwd_len = 1; // as if recorded in another directory ("/") + try testing.expect(take(®, "codedbpro") == null); + try testing.expect(!pending()); + try testing.expectEqual(@as(usize, 0), reg.servers.len); // nothing adopted; caller connects inline + _ = try reg.addServer("codedbpro", "/bin/sh", &.{ "-c", fixture_script }); + try testing.expectEqual(@as(usize, 2), reg.tools.len); +} + +test "boot prewarm: a registry with other settings is not adopted" { + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; + var reg = testRegistry(); + defer reg.deinit(); + reg.stdio_probe = true; // what GRAFF_MCP_PROBE would change + start(testing.io, testing.allocator, fixtureSpec("codedbpro", &fixture_argv)); + try testing.expect(take(®, "codedbpro") == null); + try testing.expect(!pending()); + try testing.expectEqual(@as(usize, 0), reg.tools.len); +} + +test "boot prewarm: a failed prewarm falls back to the inline connect, never to a missing tool" { + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; + var reg = testRegistry(); + defer reg.deinit(); + start(testing.io, testing.allocator, fixtureSpec("codedbpro", &.{ "-c", "exit 1" })); + try testing.expect(take(®, "codedbpro") == null); // closed fast: inline retry + try testing.expect(!pending()); + _ = try reg.addServer("codedbpro", "/bin/sh", &.{ "-c", fixture_script }); + try testing.expectEqual(@as(usize, 2), reg.tools.len); + try testing.expect(!repeatInline(error.McpHandshakeTimeout)); + try testing.expect(repeatInline(error.McpClosed)); +} + +test "boot prewarm: discard joins a pending handshake and tears it down" { + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; + start(testing.io, testing.allocator, fixtureSpec("codedbpro", &.{ "-c", "cat > /dev/null" })); // never answers + try testing.expect(pending()); + const t0 = Io.Timestamp.now(testing.io, .awake); + discard(testing.io); + try testing.expect(!pending()); + try testing.expect(t0.untilNow(testing.io, .awake).toMilliseconds() < 2_000); + discard(testing.io); // idempotent +} + +test "boot prewarm: early-exit and interactive invocations start nothing" { + const env = std.process.Environ.Map.init(testing.allocator); + try testing.expect(!wantsCompanion(.{ .help_flag = true }, &env)); + try testing.expect(!wantsCompanion(.{ .version_flag = true }, &env)); + try testing.expect(!wantsCompanion(.{}, &env)); // REPL: ADR 0230 queues it + // What args.parse builds for `graff -p hi`: the prompt is the positionals. + var words: [1][]const u8 = .{"hi"}; + const oneshot: args.Flags = .{ .print_flag = true, .positionals = .fromOwnedSlice(&words), .oneshot_prompt = "hi" }; + try testing.expect(wantsCompanion(oneshot, &env)); + var no_lean = oneshot; + no_lean.no_lean_flag = true; + try testing.expect(!wantsCompanion(no_lean, &env)); + var worktree = oneshot; + worktree.worktree_flag = "w"; + try testing.expect(!wantsCompanion(worktree, &env)); + var title: [2][]const u8 = .{ "title", "hi" }; + try testing.expect(!wantsCompanion(.{ .positionals = .fromOwnedSlice(&title), .is_subcommand = true }, &env)); + var probe = std.process.Environ.Map.init(testing.allocator); + defer probe.deinit(); + try probe.put("GRAFF_MCP_EAGER", "1"); + try testing.expect(!wantsCompanion(oneshot, &probe)); + // --help/--version return from runSubcommand before the hook; the hook + // itself sits after it in resolveKeysOrRunAcpPreAuth. + const src = @embedFile("startup.zig"); + const hook = std.mem.indexOf(u8, src, "boot_prewarm.zig\").startCompanion") orelse return error.TestExpectedHook; + try testing.expect(hook > (std.mem.indexOf(u8, src, "pub fn resolveKeysOrRunAcpPreAuth") orelse return error.TestExpectedHook)); + const main_src = @embedFile("main.zig"); + const early = std.mem.indexOf(u8, main_src, "startup.runSubcommand(") orelse return error.TestExpectedHook; + try testing.expect(early < (std.mem.indexOf(u8, main_src, "startup.resolveKeysOrRunAcpPreAuth(") orelse return error.TestExpectedHook)); + try testing.expect(!pending()); +} + +test "boot prewarm: the settings opt-out is read the way loadSkillSettings reads it" { + var arena_state = std.heap.ArenaAllocator.init(testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + try testing.expect(optedOutIn(a, "{\"skills\":{\"codedbpro\":false}}", "codedbpro")); + try testing.expect(!optedOutIn(a, "{\"skills\":{\"codedbpro\":true}}", "codedbpro")); + try testing.expect(!optedOutIn(a, "{\"skills\":{\"muonry\":false}}", "codedbpro")); + try testing.expect(!optedOutIn(a, "not json", "codedbpro")); +} diff --git a/src/companion_boot.zig b/src/companion_boot.zig index 32fff0945..3942e9eb2 100644 --- a/src/companion_boot.zig +++ b/src/companion_boot.zig @@ -12,6 +12,7 @@ const Io = std.Io; const Allocator = std.mem.Allocator; const args = @import("args.zig"); +const boot_prewarm = @import("boot_prewarm.zig"); const engine_events = @import("engine_events.zig"); const engine_sink = @import("engine_sink.zig"); const mcp = @import("mcp.zig"); @@ -62,6 +63,7 @@ pub fn connectCompanion(io: Io, arena: Allocator, registry: *mcp.Registry, flags const sink = engine_sink.writerSink(out); const speak = interactive(flags, json_mode) and environ_map.get("GRAFF_REPL_DEBUG") != null; const background = deferCompanion(flags, json_mode); + defer boot_prewarm.discard(io); // a prewarm this call did not adopt is stopped here connect: { for (skills.companion_servers) |c| { if (mcp_boot.alreadyStarting(registry, c.server)) break :connect; @@ -75,7 +77,7 @@ pub fn connectCompanion(io: Io, arena: Allocator, registry: *mcp.Registry, flags break; } if (speak) sink.emit(io, dimNotice(try std.fmt.allocPrint(arena, "companion: connecting {s}...", .{c.bin}))); - if (registry.addServer(c.server, c.bin, &.{"--mcp"})) |_| { + if (connectNow(registry, c.server, c.bin)) |_| { break; } else |err| { if (speak) sink.emit(io, dimNotice(try std.fmt.allocPrint(arena, "[mcp:{s}] auto-connect failed ({t}) — native tools only", .{ c.server, err }))); @@ -84,6 +86,13 @@ pub fn connectCompanion(io: Io, arena: Allocator, registry: *mcp.Registry, flags } } +/// The joined boot prewarm when it ran this companion, else `addServer` on +/// this thread, as before. +fn connectNow(registry: *mcp.Registry, server: []const u8, bin: []const u8) !void { + if (boot_prewarm.take(registry, server)) |joined| return joined; + _ = try registry.addServer(server, bin, &.{"--mcp"}); +} + test "deferCompanion: every interactive session, not -p or --json (ADR 0230)" { try std.testing.expect(deferCompanion(.{ .yolo_flag = true }, false)); try std.testing.expect(deferCompanion(.{}, false)); // interactive without --yolo diff --git a/src/main.zig b/src/main.zig index a618251d8..abe49f4c5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -567,7 +567,7 @@ test { // ── Unit tests (`zig build test`): pull in tests from imported modu _ = .{ @import("agent_empty_completion.zig"), @import("agent_unfinished_bounce.zig"), @import("agent_length_cutoff.zig"), @import("agent_model_loop.zig"), @import("publication_policy_tests.zig"), @import("jobs_completion_tests.zig"), @import("headless_result.zig"), @import("oneshot.zig"), @import("agent_length.zig"), @import("agent_length_tests.zig") }; _ = .{ @import("provider_routing_tests.zig"), @import("codegraff_messages_tests.zig"), @import("tool_result_empty_tests.zig"), @import("history_wire.zig"), @import("path_lookup.zig"), @import("agents_prepare.zig"), @import("catalog_price.zig"), @import("readline_paste_number_tests.zig"), @import("test_hooks.zig"), @import("peer_wake_loop.zig") }; _ = .{ @import("list_dir_nearmiss.zig"), @import("tool_preview_tests.zig"), @import("stream_reconnect.zig"), @import("oauth_kimi.zig"), @import("signin_link.zig"), @import("followups.zig") }; - _ = .{ @import("repo_map.zig"), @import("cli_text.zig") }; + _ = .{ @import("repo_map.zig"), @import("cli_text.zig"), @import("boot_prewarm.zig") }; _ = @import("agent_overflow_tests.zig"); // #414: and, through it, agent_overflow.zig's table tests _ = @import("agent_gateway_retry.zig"); // #1019: flake vs overflow classification must stay reachable _ = .{ @import("agent_responses.zig"), @import("compaction_checkpoint_tests.zig"), @import("agent_context_meter_tests.zig") }; // Responses failures, checkpoint recovery, and context accounting diff --git a/src/mcp.zig b/src/mcp.zig index 9b7074943..637e6a9e9 100644 --- a/src/mcp.zig +++ b/src/mcp.zig @@ -108,6 +108,9 @@ pub const Registry = struct { task_arenas: []std.heap.ArenaAllocator = &.{}, /// Unjoined startServer futures from a deferred --yolo boot. pending_starts: []mcp_boot.PendingStart = &.{}, + /// Set by `deinit` so a start pausing before its retry stops pausing + /// (mcp_start.Ctx.abort). Heap-owned; created with the first start. + retry_abort: ?*Io.Event = null, /// Names paired with `pending_starts` so companion auto-connect can skip /// a second spawn of a server the deferred fan-out already queued. pending_names: []const []const u8 = &.{}, @@ -429,12 +432,16 @@ pub const Registry = struct { /// `deinit` because tests and mid-session callers must never exit. pub fn deinitAtExit(reg: *Registry) void { shutdown_trace.mark("mcp-teardown"); // #364 + @import("boot_prewarm.zig").discard(reg.io); // a companion prewarm nobody adopted reg.deinit(); mcp_teardown.exitIfAbandoned(); } pub fn deinit(reg: *Registry) void { + if (reg.retry_abort) |event| event.set(reg.io); // exit does not wait out a retry pause _ = mcp_boot.joinPending(reg); + if (reg.retry_abort) |event| reg.gpa.destroy(event); + reg.retry_abort = null; const budget: mcp_teardown.Budget = .init(reg.io, mcp_teardown.teardown_grace); for (reg.servers) |server| deinitServer(server, reg.io, budget); for (reg.task_arenas) |*ta| ta.deinit(); diff --git a/src/mcp_boot.zig b/src/mcp_boot.zig index 6388d0550..f3e776109 100644 --- a/src/mcp_boot.zig +++ b/src/mcp_boot.zig @@ -96,6 +96,7 @@ pub fn initWithMerged(gpa: Allocator, io: Io, config_path: []const u8, global_pa // concurrently, and a hash-map iterator is not a thing tasks can share. const Entry = struct { name: []const u8, cfg: std.json.ObjectMap }; var entries: std.ArrayList(Entry) = .empty; + armRetryAbort(®); var it = merged.servers.iterator(); const state = @import("mcp_state.zig").load(io, a, home); // `graff mcp enable|disable` while (it.next()) |entry| { @@ -148,7 +149,29 @@ fn alreadyStartingLocked(reg: *const Registry, name: []const u8) bool { } fn startCtx(reg: *const Registry) StartCtx { - return .{ .gpa = reg.gpa, .io = reg.io, .home = reg.home, .show_diagnostics = reg.show_diagnostics, .stdio_probe = reg.stdio_probe, .lazy_stdio = reg.lazy_stdio }; + return .{ .gpa = reg.gpa, .io = reg.io, .home = reg.home, .show_diagnostics = reg.show_diagnostics, .stdio_probe = reg.stdio_probe, .lazy_stdio = reg.lazy_stdio, .abort = reg.retry_abort }; +} + +/// The event `Registry.deinit` sets so a start waiting to retry stops +/// waiting. Without one (no memory) a task keeps the plain pause. +fn armRetryAbort(reg: *Registry) void { + if (reg.retry_abort != null) return; + const event = reg.gpa.create(Io.Event) catch return; + event.* = .unset; + reg.retry_abort = event; +} + +/// Merge a connect that ran before the registry existed (boot_prewarm) where +/// `addServer` would have put it: after the servers already present, under +/// the registry mutex (ADR 0197). False when the name is already connected; +/// the latecomer is then torn down, as for a deferred start. +pub fn adoptStarted(reg: *Registry, outcome: StartOutcome) bool { + reg.mutex.lockUncancelable(reg.io); + defer reg.mutex.unlock(reg.io); + const before = reg.servers.len; + var one = [_]PendingStart{.{ .future = .{ .any_future = null, .result = outcome } }}; + mergeOutcomes(reg, &one); + return reg.servers.len > before; } /// Queue a stdio server onto `pending_starts` without waiting. Used by @@ -215,6 +238,7 @@ fn queueLocked(reg: *Registry, name: []const u8, cfg: std.json.ObjectMap) bool { return false; }; flag.* = .init(false); + armRetryAbort(reg); var ctx = startCtx(reg); ctx.ready = flag; const fut = reg.io.concurrent(startServerTask, .{ ctx, n, cfg }) catch { diff --git a/src/mcp_start.zig b/src/mcp_start.zig index 96d7d35b8..86fee6c4c 100644 --- a/src/mcp_start.zig +++ b/src/mcp_start.zig @@ -26,19 +26,34 @@ pub const Ctx = struct { ready: ?*std.atomic.Value(bool) = null, /// Pause before the one retry of a transient failure. retry_ms: u32 = 1_500, + /// The owning registry's teardown signal (`Registry.retry_abort`). Set, + /// it ends the pause at once and skips the retry, so exit never waits + /// out the pause (the registry still joins the task: nothing outlives + /// `io`, #325). Heap-owned: the registry value moves after tasks start. + abort: ?*Io.Event = null, }; pub fn task(ctx: Ctx, name: []const u8, cfg: std.json.ObjectMap) StartOutcome { defer if (ctx.ready) |flag| flag.store(true, .release); return attempt(ctx, name, cfg) catch |first| { - if (transient(first)) { - ctx.io.sleep(.fromMilliseconds(ctx.retry_ms), .awake) catch {}; + if (transient(first) and pauseBeforeRetry(ctx)) { if (attempt(ctx, name, cfg)) |outcome| return outcome else |again| return failed(ctx, name, cfg, again); } return failed(ctx, name, cfg, first); }; } +/// False when the pause was cut short: the registry is tearing down, or the +/// task was canceled. The first failure is then the one recorded. +pub fn pauseBeforeRetry(ctx: Ctx) bool { + const event = ctx.abort orelse { + ctx.io.sleep(.fromMilliseconds(ctx.retry_ms), .awake) catch return false; + return true; + }; + event.waitTimeout(ctx.io, .{ .duration = .{ .raw = .fromMilliseconds(ctx.retry_ms), .clock = .awake } }) catch |err| return err == error.Timeout; + return false; +} + fn failed(ctx: Ctx, name: []const u8, cfg: std.json.ObjectMap, err: anyerror) StartOutcome { if (ctx.show_diagnostics) std.debug.print(" [mcp:{s}] failed to start: {t}\n", .{ name, err }); note(name, err); @@ -143,6 +158,41 @@ test "a transient start failure is retried once; a missing binary is not" { try std.testing.expect(!transient(error.BadMcpConfig)); } +test "a retry pause ends at once when the registry tears down, or runs when it does not" { + const io = std.testing.io; + var event: Io.Event = .unset; + const ctx: Ctx = .{ .gpa = std.testing.allocator, .io = io, .home = "", .show_diagnostics = false, .stdio_probe = false, .retry_ms = 5, .abort = &event }; + try std.testing.expect(pauseBeforeRetry(ctx)); // not tearing down: pause, then retry + event.set(io); + var slow = ctx; + slow.retry_ms = 60_000; + const t0 = Io.Timestamp.now(io, .awake); + try std.testing.expect(!pauseBeforeRetry(slow)); + try std.testing.expect(t0.untilNow(io, .awake).toMilliseconds() < 200); +} + +test "teardown with a pending transient retry returns in well under the pause" { + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; // /bin/sh + const io = std.testing.io; + var reg = mcp.Registry.empty(std.testing.allocator, io); + reg.stdio_probe = false; + reg.lazy_stdio = false; + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + var cfg: std.json.ObjectMap = .empty; + try cfg.put(a, "command", .{ .string = "/bin/sh" }); + var argv = std.json.Array.init(a); + try argv.appendSlice(&.{ .{ .string = "-c" }, .{ .string = "exit 1" } }); // closes during the handshake: transient + try cfg.put(a, "args", .{ .array = argv }); + try std.testing.expect(@import("mcp_boot.zig").queueConfig(®, "teardown-retry-fixture", cfg)); + try io.sleep(.fromMilliseconds(300), .awake); // first attempt fails; the task is in its 1.5s pause + const t0 = Io.Timestamp.now(io, .awake); + reg.deinit(); // joins the task: nothing is left holding `io` (#325) + try std.testing.expect(t0.untilNow(io, .awake).toMilliseconds() < 200); + try std.testing.expect(failure("teardown-retry-fixture") != null); // the first failure is still named +} + test "start failures are remembered by name until the server connects" { note("fixture-a", error.FileNotFound); note("fixture-b", error.McpClosed); diff --git a/src/startup.zig b/src/startup.zig index 11348b3d1..3f262ec60 100644 --- a/src/startup.zig +++ b/src/startup.zig @@ -234,8 +234,8 @@ fn serveConfig(flags: args.Flags, environ: anytype) serve.ServeConfig { /// every other terminal trajectory keeps resolveKeys' existing fatal behavior. pub fn resolveKeysOrRunAcpPreAuth(io: Io, gpa: Allocator, arena: Allocator, init: std.process.Init, flags: args.Flags) !?ResolvedKeys { const worker_hint = flags.subagent_provider_flag orelse init.environ_map.get("GRAFF_SUBAGENT_PROVIDER"); - // Before key resolution: the startup catalog load reads it (catalog_selection.startupLoads). - if (flags.allow_cross_provider_subagents_flag) @import("subagent_selection.zig").g_cross_provider_consent = true; + if (flags.allow_cross_provider_subagents_flag) @import("subagent_selection.zig").g_cross_provider_consent = true; // before key resolution: the startup catalog load reads it (catalog_selection.startupLoads) + @import("boot_prewarm.zig").startCompanion(io, gpa, init.environ_map, flags); // -p: overlap the companion handshake with the rest of boot const is_acp = (comptime build_options.acp) and flags.positionals.items.len > 0 and std.mem.eql(u8, flags.positionals.items[0], "acp"); if (!is_acp) return try resolveKeys(io, gpa, arena, init.environ_map, flags.model_flag, worker_hint); if (try startup_keys.resolveKeysOptional(io, gpa, arena, init.environ_map, flags.model_flag, worker_hint)) |resolved| return resolved; From 04096dcc1ea74b0970483f5e9f4a91f7ef53a85f Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:42:44 +0800 Subject: [PATCH 05/21] acp: answer initialize during boot Each boot phase mark polls stdin without blocking. A complete first line that is an initialize request is taken off the one shared stdin reader and answered through the engine's handleLine. The loop dispatches it first with its output discarded, so Dispatch and the latches match the late path. Any other first line is left for the loop. The decision is acp_early.step, modelled by the AcpEarlyInit Lean kernel: initialize is answered once, the replay writes nothing, and stdout and dispatch history equal the late path's on every boot trace. A conformance fixture runs the real step and engine on every cell. Co-Authored-By: Codegraff --- build.zig | 1 + docs/adr/DRAFT-acp-early-initialize.md | 68 + lean-proofs/Graff.lean | 1 + lean-proofs/Graff/AcpEarlyInit.lean | 405 + lean-proofs/KERNELS.md | 7 +- spec/conformance.py | 11 +- spec/kernels/acp_early_init.json | 9026 +++++++++++++++++++++++ spec/manifest.json | 13 + spec/ref/acp_early_init.py | 305 + src/acp.zig | 9 +- src/acp_early.zig | 203 + src/acp_early_tests.zig | 260 + src/main.zig | 8 +- src/spec_acp_early_init_conformance.zig | 95 + src/tool_gates.zig | 1 + 15 files changed, 10403 insertions(+), 10 deletions(-) create mode 100644 docs/adr/DRAFT-acp-early-initialize.md create mode 100644 lean-proofs/Graff/AcpEarlyInit.lean create mode 100644 spec/kernels/acp_early_init.json create mode 100644 spec/ref/acp_early_init.py create mode 100644 src/acp_early.zig create mode 100644 src/acp_early_tests.zig create mode 100644 src/spec_acp_early_init_conformance.zig diff --git a/build.zig b/build.zig index 62a570a51..21ba61b12 100644 --- a/build.zig +++ b/build.zig @@ -156,6 +156,7 @@ pub fn build(b: *std.Build) void { unit_tests.root_module.addAnonymousImport("spec_prompt_cache", .{ .root_source_file = b.path("spec/kernels/prompt_cache.json") }); unit_tests.root_module.addAnonymousImport("spec_prompt_prefix", .{ .root_source_file = b.path("spec/kernels/prompt_prefix.json") }); unit_tests.root_module.addAnonymousImport("spec_prompt_stable", .{ .root_source_file = b.path("spec/kernels/prompt_stable.json") }); + unit_tests.root_module.addAnonymousImport("spec_acp_early_init", .{ .root_source_file = b.path("spec/kernels/acp_early_init.json") }); const run_tests = b.addRunArtifact(unit_tests); const test_step = b.step("test", "Run unit and subprocess integration tests"); test_step.dependOn(&run_tests.step); diff --git a/docs/adr/DRAFT-acp-early-initialize.md b/docs/adr/DRAFT-acp-early-initialize.md new file mode 100644 index 000000000..f25d5f385 --- /dev/null +++ b/docs/adr/DRAFT-acp-early-initialize.md @@ -0,0 +1,68 @@ +# DRAFT. ACP answers `initialize` during boot + +Status: proposed. The integrator assigns the number and adds the index row. + +## Context + +An ACP client spawns `graff acp` and sends `initialize` first. The dispatch +loop started only after the root Agent existed, so the answer waited for the +whole boot: worktree identity, MCP registry, settings, companion, approvals, +system prompt and root construction. The response itself reads none of that. +It depends on the request, the `GRAFF_ACP_V2` gate (ADR 0206), the version +string and whether the dispatch can load sessions. The credential-free path +already answers it without an Agent. + +Answering earlier has three ways to go wrong. The request could be answered +twice, or not at all. A stdin byte could be lost or reordered between an +early reader and the loop's reader. Or the live `Dispatch` and the process +latches that `initialize` sets could differ from the late path's. + +## Decision + +- Each boot phase mark is also a check, and so is the point where stdin and + stdout are created. The check runs on the main thread. It reads stdin only + while `poll` reports it readable, so boot never waits for the client. It + never reads past a full buffer, and it never reads outside ACP. +- There is one stdin reader. The ACP inbox pump later reads the same reader, + so the bytes after the first line stay where they were. +- A complete first line that is an `initialize` request with an id is taken + off the reader. The engine's own `handleLine` answers it on a fresh + dispatch whose `can_load` matches the live one. The answer is written only + to a clean protocol stdout. The line is kept. +- Any other first line (no `initialize`, a notification, invalid JSON, or a + line larger than the buffer) is left in place. So is an incomplete one. + After a non-`initialize` first line the checks stop. +- When the loop starts, the kept line is dispatched through the normal loop + body first, and its output is discarded. The live dispatch fields and the + latches are rebuilt by the code the late path runs. The latches are + last-writer-wins: the v2 message-id seed is the live dispatch's. +- One pure `step` makes the decision. It is modelled in + `lean-proofs/Graff/AcpEarlyInit.lean`, and the Zig code calls it. The + model proves, for every boot trace: + - the first `initialize` is answered exactly once; + - the replay writes nothing; + - a non-`initialize` first line reaches the loop unchanged; + - stdout and the dispatch history equal the late path's. + A 360-cell cube shows that the early answer plus the replay leaves the same + bytes and latches as the late path. The conformance test runs the real + engine on every cell. +- No thread is added, so no TLA+ model is needed (`formal/README.md`). + +## Consequences + +`initialize` arrives within one boot phase of the request instead of after +root construction. `session/new` and everything after it still wait for the +loop. The no-credential pre-auth path is unchanged. + +Differences from the late path: + +- If boot fails after the early answer, the client has seen `initialize` + succeed and then sees EOF. Before this change it saw EOF alone. +- The handshake log's `startup` phase still measures time to the loop. It + times the replay, not when the client was answered. +- A request that arrives during root construction, the longest phase, is + answered at the `root agent` mark, only slightly before the loop would + have answered it. Boot code inside a long phase can call + `acp_early.check()` between its steps to narrow the gap. +- Windows has no `poll` on stdin. There, only bytes already buffered count, + which in practice means the late path. diff --git a/lean-proofs/Graff.lean b/lean-proofs/Graff.lean index 5f531ab81..5eb8ff505 100644 --- a/lean-proofs/Graff.lean +++ b/lean-proofs/Graff.lean @@ -13,3 +13,4 @@ import Graff.PromptStable import Graff.ResourceKernel import Graff.AcpLatency import Graff.ImageDelivery +import Graff.AcpEarlyInit diff --git a/lean-proofs/Graff/AcpEarlyInit.lean b/lean-proofs/Graff/AcpEarlyInit.lean new file mode 100644 index 000000000..770856b33 --- /dev/null +++ b/lean-proofs/Graff/AcpEarlyInit.lean @@ -0,0 +1,405 @@ +/- + ACP early `initialize` kernel. + + `graff acp` answers the client's `initialize` during boot, at a phase + boundary, instead of after the root Agent exists. This kernel is the + decision and the protocol around it: + + (a) Process kernel: `step` over `State` × `Event` is the one decision the + Zig code consults (`src/acp_early.zig` `step`). `Sys` wraps it with + the shared stdin buffer, the protocol stdout and the dispatch log, and + the theorems quantify over every boot trace: the first `initialize` is + answered exactly once, the loop-start replay writes nothing, a first + line that is not `initialize` reaches the loop unchanged, and the early + path ends with the same stdout and dispatch history as the late path. + (b) Cube: the `initialize` response and the latches it sets, over + protocolVersion × v2 gate × subagent capabilities × `can_load` × + client extras. The early answer is written on a scratch dispatch and + then replayed on the live one; the latches are last-writer-wins, so + early + replay equals late in every cell. + + What it is NOT: a timing claim (when a boundary runs, or how fast), a model + of the non-blocking poll, or a proof that the Zig code refines it beyond the + exported fixture (`src/spec_acp_early_init_conformance.zig` runs the real + engine on every cube cell and the real `step` on every table cell). + Executable port: spec/ref/acp_early_init.py. +-/ + +namespace Graff.AcpEarlyInit + +/-! ## (a) The decision -/ + +inductive State where + | booting | answered | passed | loop +deriving DecidableEq, Repr, BEq + +/-- What the shared stdin buffer holds at a check. -/ +inductive Line where + | none | incomplete | initialize | other +deriving DecidableEq, Repr, BEq + +/-- `acp` is ACP mode with a clean protocol stdout. -/ +inductive Event where + | boundary (acp : Bool) (line : Line) + | loopStart +deriving DecidableEq, Repr, BEq + +inductive Action where + | none | answer | replay +deriving DecidableEq, Repr, BEq + +def step : State → Event → State × Action + | .booting, .boundary false _ => (.passed, .none) + | .booting, .boundary true .none => (.booting, .none) + | .booting, .boundary true .incomplete => (.booting, .none) + | .booting, .boundary true .initialize => (.answered, .answer) + | .booting, .boundary true .other => (.passed, .none) + | .answered, .boundary _ _ => (.answered, .none) + | .passed, .boundary _ _ => (.passed, .none) + | .loop, .boundary _ _ => (.loop, .none) + | .answered, .loopStart => (.loop, .replay) + | .booting, .loopStart => (.loop, .none) + | .passed, .loopStart => (.loop, .none) + | .loop, .loopStart => (.loop, .none) + +def allStates : List State := [.booting, .answered, .passed, .loop] +def allLines : List Line := [.none, .incomplete, .initialize, .other] +def allEvents : List Event := + ([false, true].flatMap fun acp => allLines.map fun l => Event.boundary acp l) ++ [.loopStart] + +theorem step_cells : allStates.length * allEvents.length = 36 := by native_decide + +/-- Only a booting process in ACP mode with a complete `initialize` answers. -/ +theorem step_answer_iff : + allStates.all (fun s => allEvents.all fun e => + ((step s e).2 == .answer) == (s == .booting && e == .boundary true .initialize)) = true := by + native_decide + +/-- Only loop start replays, and only after an early answer. -/ +theorem step_replay_iff : + allStates.all (fun s => allEvents.all fun e => + ((step s e).2 == .replay) == (s == .answered && e == .loopStart)) = true := by + native_decide + +/-- Loop start always reaches the loop, and the loop is absorbing. -/ +theorem loop_start_reaches_loop (s : State) : (step s .loopStart).1 = .loop := by + cases s <;> rfl + +theorem loop_absorbing (e : Event) : step .loop e = (.loop, .none) := by + cases e with + | boundary acp l => cases acp <;> cases l <;> rfl + | loopStart => rfl + +/-- Once answered, no boundary answers again. -/ +theorem answered_stays (acp : Bool) (l : Line) : + step .answered (.boundary acp l) = (.answered, .none) := by + cases acp <;> cases l <;> rfl + +/-! ## (a) The protocol around it -/ + +/-- A stdin line, tagged with its position. -/ +inductive Msg where + | init (i : Nat) + | other (i : Nat) +deriving DecidableEq, Repr, BEq + +/-- A stdout line answering the request at position `i`. -/ +inductive Out where + | resp (i : Nat) +deriving DecidableEq, Repr, BEq + +def answerOf : Msg → Out + | .init i => .resp i + | .other i => .resp i + +structure Sys where + st : State + /-- The first line is complete in the buffer. -/ + avail : Bool + /-- Unconsumed stdin (the one reader boot and loop share). -/ + input : List Msg + /-- The consumed `initialize`, kept for the replay. -/ + stash : Option Msg + /-- Protocol stdout. -/ + out : List Out + /-- Lines the live dispatch has handled, in order; its state is a fold of these. -/ + disp : List Msg +deriving DecidableEq, Repr + +def start (input : List Msg) : Sys := ⟨.booting, false, input, none, [], []⟩ + +def classify (s : Sys) : Line := + if s.avail then + match s.input with + | [] => .none + | .init _ :: _ => .initialize + | .other _ :: _ => .other + else .incomplete + +inductive BootEv where + /-- The client's first line becomes complete. -/ + | arrive + /-- A boot phase boundary checks. -/ + | boundary +deriving DecidableEq, Repr, BEq + +def bootStep (acp : Bool) (s : Sys) : BootEv → Sys + | .arrive => { s with avail := true } + | .boundary => + match step s.st (.boundary acp (classify s)), s.input with + | (st, .answer), m :: rest => { s with st := st, input := rest, stash := some m, out := s.out ++ [answerOf m] } + | (st, _), _ => { s with st := st } + +def boot (acp : Bool) (s : Sys) : List BootEv → Sys + | [] => s + | e :: es => boot acp (bootStep acp s e) es + +def loopStart (s : Sys) : Sys := + match step s.st .loopStart, s.stash with + | (st, .replay), some m => { s with st := st, stash := none, disp := s.disp ++ [m] } + | (st, _), _ => { s with st := st } + +def drainFrom (s : Sys) : List Msg → Sys + | [] => s + | m :: ms => drainFrom { s with disp := s.disp ++ [m], out := s.out ++ [answerOf m] } ms + +def drain (s : Sys) : Sys := drainFrom { s with input := [] } s.input + +def runEarly (acp : Bool) (input : List Msg) (tr : List BootEv) : Sys := + drain (loopStart (boot acp (start input) tr)) + +/-- Today's order: no boundary ever looks. -/ +def runLate (input : List Msg) : Sys := drain (loopStart (start input)) + +/-- What boot can have done to the start state. -/ +def Inv (input : List Msg) (s : Sys) : Prop := + s.disp = [] ∧ + ((s.st = .booting ∨ s.st = .passed) ∧ s.stash = none ∧ s.input = input ∧ s.out = [] ∨ + ∃ i rest, input = .init i :: rest ∧ s.st = .answered ∧ s.stash = some (.init i) ∧ + s.input = rest ∧ s.out = [.resp i]) + +theorem inv_start (input : List Msg) : Inv input (start input) := + ⟨rfl, Or.inl ⟨Or.inl rfl, rfl, rfl, rfl⟩⟩ + +theorem inv_bootStep (acp : Bool) (input : List Msg) (s : Sys) (e : BootEv) + (h : Inv input s) : Inv input (bootStep acp s e) := by + obtain ⟨st, avail, inp, stash, out, disp⟩ := s + obtain ⟨hd, h⟩ := h + simp only at hd h + subst hd + cases e with + | arrive => + exact ⟨rfl, h⟩ + | boundary => + rcases h with ⟨hst | hst, hs, hi, ho⟩ | ⟨i, rest, hin, hst, hs, hi, ho⟩ + · subst hst hs hi ho + cases acp <;> cases avail <;> rcases inp with _ | ⟨_ | _, _⟩ <;> + simp [bootStep, classify, step, Inv, answerOf] <;> exact ⟨_, _, ⟨rfl, rfl⟩, rfl, rfl, rfl⟩ + · subst hst hs hi ho + cases acp <;> simp [bootStep, step, Inv] + · subst hst hs hi ho hin + cases acp <;> simp [bootStep, step, Inv] <;> exact ⟨_, _, ⟨rfl, rfl⟩, rfl, rfl, rfl⟩ + +theorem inv_boot (acp : Bool) (input : List Msg) (tr : List BootEv) : + ∀ s, Inv input s → Inv input (boot acp s tr) := by + induction tr with + | nil => intro s h; exact h + | cons e es ih => intro s h; exact ih _ (inv_bootStep acp input s e h) + +theorem drainFrom_out (s : Sys) (ms : List Msg) : + (drainFrom s ms).out = s.out ++ ms.map answerOf := by + induction ms generalizing s with + | nil => simp [drainFrom] + | cons m ms ih => simp [drainFrom, ih, List.append_assoc] + +theorem drainFrom_disp (s : Sys) (ms : List Msg) : + (drainFrom s ms).disp = s.disp ++ ms := by + induction ms generalizing s with + | nil => simp [drainFrom] + | cons m ms ih => simp [drainFrom, ih, List.append_assoc] + +theorem drain_out (s : Sys) : (drain s).out = s.out ++ s.input.map answerOf := drainFrom_out _ _ +theorem drain_disp (s : Sys) : (drain s).disp = s.disp ++ s.input := drainFrom_disp _ _ + +/-- The late path answers every line once, in order, and dispatches every line. -/ +theorem late_out (input : List Msg) : (runLate input).out = input.map answerOf := by + simp [runLate, drain_out, loopStart, start, step] + +theorem late_disp (input : List Msg) : (runLate input).disp = input := by + simp [runLate, drain_disp, loopStart, start, step] + +/-- The loop-start replay writes nothing on stdout. -/ +theorem replay_silent (s : Sys) : (loopStart s).out = s.out := by + unfold loopStart + split <;> rfl + +/-- Early and late write the same stdout, line for line. -/ +theorem early_out_eq_late (acp : Bool) (input : List Msg) (tr : List BootEv) : + (runEarly acp input tr).out = (runLate input).out := by + have h := inv_boot acp input tr (start input) (inv_start input) + rw [late_out] + unfold runEarly + generalize boot acp (start input) tr = s at h + obtain ⟨st, avail, inp, stash, out, disp⟩ := s + obtain ⟨hd, h⟩ := h + simp only at hd h + rcases h with ⟨hst | hst, hs, hi, ho⟩ | ⟨i, rest, hin, hst, hs, hi, ho⟩ <;> + subst hd <;> (try subst hst) <;> (try subst hs) <;> (try subst hi) <;> (try subst ho) <;> + (try subst hin) <;> simp [drain_out, loopStart, step, answerOf] + +/-- Early and late leave the live dispatch with the same history, hence the same state. -/ +theorem early_disp_eq_late (acp : Bool) (input : List Msg) (tr : List BootEv) : + (runEarly acp input tr).disp = (runLate input).disp := by + have h := inv_boot acp input tr (start input) (inv_start input) + rw [late_disp] + unfold runEarly + generalize boot acp (start input) tr = s at h + obtain ⟨st, avail, inp, stash, out, disp⟩ := s + obtain ⟨hd, h⟩ := h + simp only at hd h + rcases h with ⟨hst | hst, hs, hi, ho⟩ | ⟨i, rest, hin, hst, hs, hi, ho⟩ <;> + subst hd <;> (try subst hst) <;> (try subst hs) <;> (try subst hi) <;> (try subst ho) <;> + (try subst hin) <;> simp [drain_disp, loopStart, step] + +/-- A first `initialize` is answered exactly once on every boot trace. -/ +theorem initialize_answered_once (acp : Bool) (i : Nat) (rest : List Msg) (tr : List BootEv) + (fresh : ∀ m ∈ rest, answerOf m ≠ .resp i) : + (runEarly acp (.init i :: rest) tr).out.filter (fun o => decide (o = .resp i)) = [.resp i] := by + rw [early_out_eq_late, late_out] + simp only [List.map_cons, answerOf, List.filter_cons, decide_true, if_true, List.cons.injEq, true_and] + rw [List.filter_eq_nil_iff] + intro o ho + simp only [List.mem_map] at ho + obtain ⟨m, hm, rfl⟩ := ho + simpa using fresh m hm + +/-- Boot writes at most the one answer, and never touches the live dispatch. -/ +theorem boot_writes_at_most_one (acp : Bool) (input : List Msg) (tr : List BootEv) : + (boot acp (start input) tr).out.length ≤ 1 ∧ (boot acp (start input) tr).disp = [] := by + obtain ⟨hd, h⟩ := inv_boot acp input tr (start input) (inv_start input) + refine ⟨?_, hd⟩ + rcases h with ⟨_, _, _, ho⟩ | ⟨i, rest, _, _, _, _, ho⟩ <;> simp [ho] + +/-- A first line that is not `initialize` reaches the loop unchanged. -/ +theorem other_first_untouched (acp : Bool) (k : Nat) (rest : List Msg) (tr : List BootEv) : + (boot acp (start (.other k :: rest)) tr).input = .other k :: rest ∧ + (boot acp (start (.other k :: rest)) tr).stash = none ∧ + (boot acp (start (.other k :: rest)) tr).out = [] := by + obtain ⟨_, h⟩ := inv_boot acp _ tr (start _) (inv_start (.other k :: rest)) + rcases h with ⟨_, hs, hi, ho⟩ | ⟨i, r, hin, _⟩ + · exact ⟨hi, hs, ho⟩ + · cases hin + +/-- A check that sees no complete first line consumes nothing. -/ +theorem partial_not_consumed (acp : Bool) (s : Sys) (h : s.avail = false) : + (bootStep acp s .boundary).input = s.input ∧ (bootStep acp s .boundary).out = s.out := by + obtain ⟨st, avail, inp, stash, out, disp⟩ := s + simp only at h + subst h + cases st <;> cases acp <;> simp [bootStep, classify, step] + +/-! ## (b) The response cube -/ + +inductive Pv where + | missing | v0 | v1 | v2 | v9 +deriving DecidableEq, Repr, BEq + +def Pv.requested : Pv → Nat + | .missing => 1 | .v0 => 0 | .v1 => 1 | .v2 => 2 | .v9 => 9 + +/-- `clientCapabilities.subagents`: absent, an object, or JSON null. -/ +inductive Sub where + | absent | object | null +deriving DecidableEq, Repr, BEq + +/-- `clientCapabilities._meta["graff/backgroundSubagents"]`. -/ +inductive Bg where + | absent | yes | no +deriving DecidableEq, Repr, BEq + +structure Cell where + pv : Pv + gate : Bool + sub : Sub + bg : Bg + canLoad : Bool + /-- T3 clientInfo + elicitation form + session compaction + v2 terminal auth. -/ + extras : Bool +deriving DecidableEq, Repr, BEq + +def allCells : List Cell := Id.run do + let mut acc : List Cell := [] + for pv in [Pv.missing, .v0, .v1, .v2, .v9] do + for gate in [false, true] do + for sub in [Sub.absent, .object, .null] do + for bg in [Bg.absent, .yes, .no] do + for canLoad in [false, true] do + for extras in [false, true] do + acc := acc ++ [{ pv, gate, sub, bg, canLoad, extras }] + return acc + +theorem cube_cells : allCells.length = 360 := by native_decide + +/-- `acp_v2.negotiated`: v2 only behind the gate and on request (ADR 0206). -/ +def isV2 (c : Cell) : Bool := c.gate && decide (c.pv.requested ≥ 2) + +structure Resp where + protocolVersion : Nat + v2Shape : Bool + canLoad : Bool + authTerminal : Bool +deriving DecidableEq, Repr, BEq + +/-- `respondInitialize`: reads the cell and the dispatch's `can_load`, nothing else. -/ +def resp (c : Cell) (canLoad : Bool) : Resp := + { protocolVersion := if isV2 c then 2 else 1, v2Shape := isV2 c, canLoad, + authTerminal := !isV2 c || c.extras } + +/-- Process latches plus the dispatch fields `initialize` writes. -/ +structure Latch where + v2 : Bool + seed : Nat + t3 : Bool + elicit : Bool + compaction : Bool + subagents : Bool + bg : Bool +deriving DecidableEq, Repr, BEq + +def bootLatch : Latch := ⟨false, 0, false, false, false, false, false⟩ + +/-- Handling `initialize` with dispatch seed `seed`. The v2 message-id seed +is written only on a v2 connection; every other latch is overwritten. -/ +def latch (l : Latch) (c : Cell) (seed : Nat) : Latch := + { v2 := isV2 c, seed := if isV2 c then seed else l.seed, t3 := c.extras, elicit := c.extras, + compaction := c.extras || isV2 c, subagents := !isV2 c && c.sub == .object, bg := c.bg == .yes } + +/-- Late: the live dispatch (seed `s`) handles the line and writes the response. -/ +def late (l : Latch) (c : Cell) (s : Nat) : Latch × Resp := (latch l c s, resp c c.canLoad) + +/-- Early: a scratch dispatch (seed 0) writes the response; the replay on the +live dispatch writes the latches again and its response is discarded. -/ +def early (l : Latch) (c : Cell) (s : Nat) : Latch × Resp := + (latch (latch l c 0) c s, resp c c.canLoad) + +theorem latch_last_writer_wins (l : Latch) (c : Cell) (s₀ s : Nat) : + latch (latch l c s₀) c s = latch l c s := by + unfold latch + cases isV2 c <;> simp + +theorem early_eq_late (l : Latch) (c : Cell) (s : Nat) : early l c s = late l c s := by + simp [early, late, latch_last_writer_wins] + +theorem cube_early_eq_late : + allCells.all (fun c => [0, 1, 0x5eed].all fun s => early bootLatch c s == late bootLatch c s) = true := by + native_decide + +/-- The draft child-session stream is a v1 shape: v2 never latches it. -/ +theorem cube_v2_no_subagents : + allCells.all (fun c => !(isV2 c && (late bootLatch c 7).1.subagents)) = true := by + native_decide + +/-- Exactly the gated cells that ask for ≥ 2 answer 2. -/ +theorem cube_v2_cells : (allCells.filter isV2).length = 72 := by native_decide + +end Graff.AcpEarlyInit diff --git a/lean-proofs/KERNELS.md b/lean-proofs/KERNELS.md index 1f173e58d..2d16071cc 100644 --- a/lean-proofs/KERNELS.md +++ b/lean-proofs/KERNELS.md @@ -25,7 +25,11 @@ is delivered once or refused, never dropped; a request is in call order whichever read finished first; a trailer ref is an image block or its line; the port is the `Model` in `src/image_inbox.zig`, run against the queue over every sequence of six events). They are not Turing -machines: no tape, no halt state. +machines: no tape, no halt state. `AcpEarlyInit` (an `initialize` +answered at a boot boundary is answered once, its loop-start replay writes +nothing, a non-`initialize` first line reaches the loop unchanged, and +stdout and dispatch history equal the late path's on every boot trace) is +the decision `src/acp_early.zig` consults. The shape of a subagent fleet is not a cube. `Shape` is the observation ladder of one turn and stays a cube. How many children spawn, which @@ -65,6 +69,7 @@ not a skip: TUI, prompts, and SSE bytes stay out of Lean on purpose. |---|---|---| | `BashPolicy` | finite command cube: `isSimple`, `escapesCwd`, `readOnlyAllowed`, `readOnlyExternal` | live argv quoting, Windows cmd.exe | | `ImageDelivery` | 102 admit cells + `Event`/`step` (inbox) + the trailer promotion fold | base64 bytes, provider wire shape, which read finishes first | +| `AcpEarlyInit` | 36 `step` cells + boot-trace theorems (∀ traces) + 360 `initialize` response cells; 5 mutants rejected | a timing claim (when a boundary runs, how fast), the non-blocking poll, or Zig refinement beyond the fixture (the conformance test runs the real `step` and the real engine per cell) | ### Never (accounted for; eval / tier 2) diff --git a/spec/conformance.py b/spec/conformance.py index 539d8eab3..abf7e3ee0 100644 --- a/spec/conformance.py +++ b/spec/conformance.py @@ -68,6 +68,8 @@ from ref.bash_policy import payload as bash_model_payload from ref.structured_output import check_properties as sox_check_properties from ref.structured_output import payload as sox_model_payload +from ref.acp_early_init import check_properties as early_check_properties +from ref.acp_early_init import payload as early_payload ROOT = Path(__file__).resolve().parent KERNELS = ROOT / "kernels" @@ -82,6 +84,7 @@ SOX_FIXTURE = KERNELS / "structured_output.json" CACHE_FIXTURE = KERNELS / "prompt_cache.json" PREFIX_FIXTURE = KERNELS / "prompt_prefix.json" +EARLY_FIXTURE = KERNELS / "acp_early_init.json" LEAN_DIR = ROOT.parent / "lean-proofs" @@ -388,6 +391,7 @@ def check_bash() -> int: return _ref(bash_check_properties, "bash") def check_sox() -> int: return _ref(sox_check_properties, "sox") +def check_early() -> int: return _ref(early_check_properties, "acp_early_init") def prefixes_first(p: str) -> list[str]: @@ -420,6 +424,7 @@ def export() -> list[Path]: _write(CACHE_FIXTURE, cache_payload()), write_cache_md(KERNELS / "prompt_cache.md"), _write(PREFIX_FIXTURE, prefix_payload()), + _write(EARLY_FIXTURE, early_payload()), write_prefix_md(KERNELS / "prompt_prefix.md"), _write(KERNELS / "prompt_stable.json", __import__("ref.prompt_stable", fromlist=["payload"]).payload()), __import__("ref.prompt_stable", fromlist=["write_kernel_md"]).write_kernel_md(KERNELS / "prompt_stable.md"), @@ -445,6 +450,7 @@ def check_fixtures() -> None: _same(SOX_FIXTURE, sox_model_payload(), "structured_output") _same(CACHE_FIXTURE, cache_payload(), "prompt_cache") _same(PREFIX_FIXTURE, prefix_payload(), "prompt_prefix") + _same(EARLY_FIXTURE, early_payload(), "acp_early_init") from ref.prompt_stable import payload as stable_payload, kernel_md as stable_kernel_md _same(KERNELS / "prompt_stable.json", stable_payload(), "prompt_stable") from ref.goal_loop import kernel_md @@ -558,17 +564,18 @@ def main() -> int: n_sc = check_score() n_ba = check_bash() n_sx = check_sox() + n_ae = check_early() if args.export: paths = export() rel = ", ".join(p.relative_to(ROOT.parent).as_posix() for p in paths) - print(f"exported catalog={n_cat} transport={n_tr} provider={n_pr} goal={n_gl} cache={n_ck} prefix={n_px} stable={n_ps} path={n_pc} shape={n_sh} score={n_sc} bash={n_ba} sox={n_sx} → {rel}") + print(f"exported catalog={n_cat} transport={n_tr} provider={n_pr} goal={n_gl} cache={n_ck} prefix={n_px} stable={n_ps} path={n_pc} shape={n_sh} score={n_sc} bash={n_ba} sox={n_sx} acp_early={n_ae} → {rel}") else: check_fixtures() print( f"ok tool_catalog {n_cat} transport {n_tr} (1 ws) " f"provider {n_pr} goal_loop {n_gl} prompt_cache {n_ck} prompt_prefix {n_px} prompt_stable {n_ps} path_confine {n_pc} " f"shape {n_sh} score {n_sc} bash {n_ba} " - f"structured_output {n_sx}" + f"structured_output {n_sx} acp_early_init {n_ae}" ) if args.lean: try_lean() diff --git a/spec/kernels/acp_early_init.json b/spec/kernels/acp_early_init.json new file mode 100644 index 000000000..2d7a8e2d5 --- /dev/null +++ b/spec/kernels/acp_early_init.json @@ -0,0 +1,9026 @@ +{ + "kernel": "acp_early_init", + "version": 1, + "universe": { + "states": [ + "booting", + "answered", + "passed", + "loop" + ], + "lines": [ + "none", + "incomplete", + "initialize", + "other" + ], + "actions": [ + "none", + "answer", + "replay" + ] + }, + "mutants_rejected": [ + "answer-early-and-again-in-loop", + "replay-emits-response", + "consume-other-first-line", + "early-can-load-false", + "skip-replay" + ], + "step": [ + { + "state": "booting", + "event": { + "kind": "boundary", + "acp": false, + "line": "none" + }, + "next": "passed", + "action": "none" + }, + { + "state": "booting", + "event": { + "kind": "boundary", + "acp": false, + "line": "incomplete" + }, + "next": "passed", + "action": "none" + }, + { + "state": "booting", + "event": { + "kind": "boundary", + "acp": false, + "line": "initialize" + }, + "next": "passed", + "action": "none" + }, + { + "state": "booting", + "event": { + "kind": "boundary", + "acp": false, + "line": "other" + }, + "next": "passed", + "action": "none" + }, + { + "state": "booting", + "event": { + "kind": "boundary", + "acp": true, + "line": "none" + }, + "next": "booting", + "action": "none" + }, + { + "state": "booting", + "event": { + "kind": "boundary", + "acp": true, + "line": "incomplete" + }, + "next": "booting", + "action": "none" + }, + { + "state": "booting", + "event": { + "kind": "boundary", + "acp": true, + "line": "initialize" + }, + "next": "answered", + "action": "answer" + }, + { + "state": "booting", + "event": { + "kind": "boundary", + "acp": true, + "line": "other" + }, + "next": "passed", + "action": "none" + }, + { + "state": "booting", + "event": { + "kind": "loop_start" + }, + "next": "loop", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "boundary", + "acp": false, + "line": "none" + }, + "next": "answered", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "boundary", + "acp": false, + "line": "incomplete" + }, + "next": "answered", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "boundary", + "acp": false, + "line": "initialize" + }, + "next": "answered", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "boundary", + "acp": false, + "line": "other" + }, + "next": "answered", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "boundary", + "acp": true, + "line": "none" + }, + "next": "answered", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "boundary", + "acp": true, + "line": "incomplete" + }, + "next": "answered", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "boundary", + "acp": true, + "line": "initialize" + }, + "next": "answered", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "boundary", + "acp": true, + "line": "other" + }, + "next": "answered", + "action": "none" + }, + { + "state": "answered", + "event": { + "kind": "loop_start" + }, + "next": "loop", + "action": "replay" + }, + { + "state": "passed", + "event": { + "kind": "boundary", + "acp": false, + "line": "none" + }, + "next": "passed", + "action": "none" + }, + { + "state": "passed", + "event": { + "kind": "boundary", + "acp": false, + "line": "incomplete" + }, + "next": "passed", + "action": "none" + }, + { + "state": "passed", + "event": { + "kind": "boundary", + "acp": false, + "line": "initialize" + }, + "next": "passed", + "action": "none" + }, + { + "state": "passed", + "event": { + "kind": "boundary", + "acp": false, + "line": "other" + }, + "next": "passed", + "action": "none" + }, + { + "state": "passed", + "event": { + "kind": "boundary", + "acp": true, + "line": "none" + }, + "next": "passed", + "action": "none" + }, + { + "state": "passed", + "event": { + "kind": "boundary", + "acp": true, + "line": "incomplete" + }, + "next": "passed", + "action": "none" + }, + { + "state": "passed", + "event": { + "kind": "boundary", + "acp": true, + "line": "initialize" + }, + "next": "passed", + "action": "none" + }, + { + "state": "passed", + "event": { + "kind": "boundary", + "acp": true, + "line": "other" + }, + "next": "passed", + "action": "none" + }, + { + "state": "passed", + "event": { + "kind": "loop_start" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "boundary", + "acp": false, + "line": "none" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "boundary", + "acp": false, + "line": "incomplete" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "boundary", + "acp": false, + "line": "initialize" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "boundary", + "acp": false, + "line": "other" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "boundary", + "acp": true, + "line": "none" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "boundary", + "acp": true, + "line": "incomplete" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "boundary", + "acp": true, + "line": "initialize" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "boundary", + "acp": true, + "line": "other" + }, + "next": "loop", + "action": "none" + }, + { + "state": "loop", + "event": { + "kind": "loop_start" + }, + "next": "loop", + "action": "none" + } + ], + "cases": [ + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":10,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":11,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":12,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":14,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":15,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":16,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":17,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":18,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":19,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":20,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":21,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":22,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":23,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":24,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":25,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":26,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":27,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":28,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":29,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":30,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":31,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pvmissing.g0.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":32,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pvmissing.g0.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":33,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pvmissing.g0.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":34,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pvmissing.g0.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":35,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":36,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":37,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":38,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":39,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":40,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":41,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":43,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":44,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":45,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":46,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":47,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":48,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":49,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":50,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":51,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":52,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":53,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":54,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":55,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":56,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":57,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":58,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":59,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":60,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":61,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":62,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":63,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":64,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":65,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":66,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":67,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pvmissing.g1.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":68,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pvmissing.g1.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":69,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pvmissing.g1.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":70,\"method\":\"initialize\",\"params\":{\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": "missing", + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pvmissing.g1.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":71,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv0.g0.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":72,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv0.g0.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":73,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv0.g0.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":74,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv0.g0.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":75,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv0.g0.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":76,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv0.g0.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":77,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv0.g0.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":78,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv0.g0.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":79,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv0.g0.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":80,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv0.g0.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":81,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv0.g0.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":82,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv0.g0.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":83,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv0.g0.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":84,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv0.g0.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":85,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv0.g0.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":86,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv0.g0.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":87,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv0.g0.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":88,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv0.g0.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":89,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv0.g0.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":90,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv0.g0.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":91,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv0.g0.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":92,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv0.g0.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":93,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv0.g0.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":94,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv0.g0.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":95,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv0.g0.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":96,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv0.g0.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":97,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv0.g0.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":98,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv0.g0.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv0.g0.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":100,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv0.g0.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":101,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv0.g0.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":102,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv0.g0.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":103,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv0.g0.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":104,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv0.g0.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":105,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv0.g0.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":106,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv0.g0.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":107,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv0.g1.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":108,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv0.g1.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":109,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv0.g1.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":110,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv0.g1.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":111,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv0.g1.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":112,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv0.g1.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":113,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv0.g1.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":114,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv0.g1.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":115,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv0.g1.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":116,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv0.g1.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":117,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv0.g1.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":118,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv0.g1.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":119,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv0.g1.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":120,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv0.g1.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":121,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv0.g1.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":122,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv0.g1.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":123,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv0.g1.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":124,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv0.g1.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":125,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv0.g1.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":126,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv0.g1.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":127,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv0.g1.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":128,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv0.g1.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":129,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv0.g1.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":130,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv0.g1.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":131,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv0.g1.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":132,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv0.g1.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":133,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv0.g1.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":134,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv0.g1.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":135,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv0.g1.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":136,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv0.g1.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":137,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv0.g1.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":138,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv0.g1.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":139,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv0.g1.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":140,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv0.g1.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":141,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv0.g1.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":142,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 0, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv0.g1.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":143,\"method\":\"initialize\",\"params\":{\"protocolVersion\":0,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv1.g0.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":144,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv1.g0.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":145,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv1.g0.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":146,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv1.g0.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":147,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv1.g0.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":148,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv1.g0.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":149,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv1.g0.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":150,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv1.g0.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":151,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv1.g0.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":152,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv1.g0.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":153,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv1.g0.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":154,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv1.g0.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":155,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv1.g0.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":156,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv1.g0.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":157,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv1.g0.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":158,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv1.g0.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":159,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv1.g0.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":160,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv1.g0.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":161,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv1.g0.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":162,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv1.g0.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":163,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv1.g0.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":164,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv1.g0.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":165,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv1.g0.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":166,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv1.g0.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":167,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv1.g0.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":168,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv1.g0.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":169,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv1.g0.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":170,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv1.g0.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":171,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv1.g0.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":172,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv1.g0.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":173,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv1.g0.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":174,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv1.g0.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":175,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv1.g0.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":176,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv1.g0.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":177,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv1.g0.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":178,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv1.g0.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":179,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv1.g1.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":180,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv1.g1.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":181,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv1.g1.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":182,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv1.g1.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":183,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv1.g1.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":184,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv1.g1.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":185,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv1.g1.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":186,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv1.g1.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":187,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv1.g1.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":188,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv1.g1.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":189,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv1.g1.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":190,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv1.g1.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":191,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv1.g1.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":192,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv1.g1.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":193,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv1.g1.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":194,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv1.g1.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":195,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv1.g1.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":196,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv1.g1.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":197,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv1.g1.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":198,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv1.g1.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":199,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv1.g1.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":200,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv1.g1.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":201,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv1.g1.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":202,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv1.g1.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":203,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv1.g1.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":204,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv1.g1.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":205,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv1.g1.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":206,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv1.g1.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":207,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv1.g1.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":208,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv1.g1.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":209,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv1.g1.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":210,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv1.g1.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":211,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv1.g1.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":212,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv1.g1.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":213,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv1.g1.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":214,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 1, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv1.g1.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":215,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv2.g0.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":216,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv2.g0.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":217,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv2.g0.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":218,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv2.g0.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":219,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv2.g0.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":220,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv2.g0.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":221,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv2.g0.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":222,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv2.g0.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":223,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv2.g0.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":224,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv2.g0.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":225,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv2.g0.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":226,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv2.g0.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":227,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv2.g0.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":228,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv2.g0.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":229,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv2.g0.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":230,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv2.g0.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":231,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv2.g0.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":232,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv2.g0.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":233,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv2.g0.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":234,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv2.g0.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":235,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv2.g0.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":236,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv2.g0.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":237,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv2.g0.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":238,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv2.g0.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":239,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv2.g0.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":240,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv2.g0.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":241,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv2.g0.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":242,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv2.g0.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":243,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv2.g0.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":244,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv2.g0.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":245,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv2.g0.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":246,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv2.g0.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":247,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv2.g0.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":248,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv2.g0.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":249,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv2.g0.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":250,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv2.g0.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":251,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv2.g1.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":252,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv2.g1.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":253,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv2.g1.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":254,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv2.g1.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":255,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv2.g1.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":256,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv2.g1.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":257,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv2.g1.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":258,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv2.g1.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":259,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv2.g1.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":260,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv2.g1.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":261,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv2.g1.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":262,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv2.g1.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":263,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv2.g1.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":264,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv2.g1.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":265,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv2.g1.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":266,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv2.g1.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":267,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv2.g1.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":268,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv2.g1.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":269,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv2.g1.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":270,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv2.g1.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":271,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv2.g1.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":272,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv2.g1.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":273,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv2.g1.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":274,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv2.g1.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":275,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv2.g1.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":276,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv2.g1.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":277,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv2.g1.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":278,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv2.g1.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":279,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv2.g1.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":280,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv2.g1.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":281,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv2.g1.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":282,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv2.g1.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":283,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv2.g1.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":284,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv2.g1.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":285,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv2.g1.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":286,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 2, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv2.g1.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":287,\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv9.g0.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":288,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv9.g0.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":289,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv9.g0.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":290,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv9.g0.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":291,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv9.g0.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":292,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv9.g0.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":293,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv9.g0.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":294,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv9.g0.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":295,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv9.g0.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":296,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv9.g0.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":297,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv9.g0.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":298,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv9.g0.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":299,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv9.g0.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":300,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv9.g0.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":301,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv9.g0.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":302,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv9.g0.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":303,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv9.g0.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":304,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv9.g0.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":305,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv9.g0.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":306,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv9.g0.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":307,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv9.g0.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":308,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv9.g0.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":309,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv9.g0.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":310,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": true, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv9.g0.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":311,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": true, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv9.g0.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":312,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv9.g0.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":313,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv9.g0.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":314,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv9.g0.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":315,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv9.g0.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":316,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv9.g0.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":317,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv9.g0.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":318,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv9.g0.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":319,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv9.g0.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":320,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv9.g0.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":321,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv9.g0.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":322,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": false, + "elicit": false, + "compaction": false, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": false, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv9.g0.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":323,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 1, + "v2_shape": false, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": false, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv9.g1.subabsent.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":324,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv9.g1.subabsent.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":325,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv9.g1.subabsent.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":326,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv9.g1.subabsent.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":327,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv9.g1.subabsent.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":328,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv9.g1.subabsent.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":329,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv9.g1.subabsent.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":330,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv9.g1.subabsent.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":331,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv9.g1.subabsent.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":332,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv9.g1.subabsent.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":333,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv9.g1.subabsent.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":334,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "absent", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv9.g1.subabsent.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":335,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv9.g1.subobject.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":336,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv9.g1.subobject.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":337,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv9.g1.subobject.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":338,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv9.g1.subobject.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":339,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv9.g1.subobject.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":340,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv9.g1.subobject.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":341,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv9.g1.subobject.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":342,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv9.g1.subobject.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":343,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv9.g1.subobject.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":344,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv9.g1.subobject.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":345,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv9.g1.subobject.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":346,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "object", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv9.g1.subobject.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":347,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": false, + "id": "pv9.g1.subnull.bgabsent.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":348,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": false, + "extras": true, + "id": "pv9.g1.subnull.bgabsent.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":349,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": false, + "id": "pv9.g1.subnull.bgabsent.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":350,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "absent", + "can_load": true, + "extras": true, + "id": "pv9.g1.subnull.bgabsent.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":351,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": false, + "id": "pv9.g1.subnull.bgtrue.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":352,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": false, + "extras": true, + "id": "pv9.g1.subnull.bgtrue.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":353,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": false, + "id": "pv9.g1.subnull.bgtrue.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":354,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "true", + "can_load": true, + "extras": true, + "id": "pv9.g1.subnull.bgtrue.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":355,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": true + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": false, + "id": "pv9.g1.subnull.bgfalse.cl0.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":356,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": false, + "extras": true, + "id": "pv9.g1.subnull.bgfalse.cl0.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":357,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": false, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": false, + "id": "pv9.g1.subnull.bgfalse.cl1.x0", + "request": "{\"jsonrpc\":\"2.0\",\"id\":358,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": false + }, + "latches": { + "v2": true, + "t3": false, + "elicit": false, + "compaction": true, + "subagents": false, + "bg": false + } + }, + { + "pv": 9, + "gate": true, + "sub": "null", + "bg": "false", + "can_load": true, + "extras": true, + "id": "pv9.g1.subnull.bgfalse.cl1.x1", + "request": "{\"jsonrpc\":\"2.0\",\"id\":359,\"method\":\"initialize\",\"params\":{\"protocolVersion\":9,\"clientInfo\":{\"name\":\"t3-code\",\"version\":\"1\"},\"capabilities\":{\"auth\":{\"terminal\":{}}},\"clientCapabilities\":{\"fs\":{},\"subagents\":null,\"_meta\":{\"graff/backgroundSubagents\":false},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "response": { + "protocol_version": 2, + "v2_shape": true, + "can_load": true, + "auth_terminal": true + }, + "latches": { + "v2": true, + "t3": true, + "elicit": true, + "compaction": true, + "subagents": false, + "bg": false + } + } + ] +} diff --git a/spec/manifest.json b/spec/manifest.json index b1cfd36e0..28a5c5c84 100644 --- a/spec/manifest.json +++ b/spec/manifest.json @@ -120,6 +120,19 @@ "impl": "src/goal_state.zig:steeringGate+src/prompts.zig:pinStandingGoal+src/skill_docs.zig:execSkill", "cells_floor": 20, "cells_key": "cases" + }, + { + "id": "acp_early_init", + "lean": "lean-proofs/Graff/AcpEarlyInit.lean", + "ref": "spec/ref/acp_early_init.py", + "fixture": "spec/kernels/acp_early_init.json", + "zig": "src/spec_acp_early_init_conformance.zig", + "impl": "src/acp_early.zig:step", + "cells_floor": 396, + "cells_key": [ + "step", + "cases" + ] } ] } diff --git a/spec/ref/acp_early_init.py b/spec/ref/acp_early_init.py new file mode 100644 index 000000000..54ab8c5fc --- /dev/null +++ b/spec/ref/acp_early_init.py @@ -0,0 +1,305 @@ +"""Executable port of lean-proofs/Graff/AcpEarlyInit.lean. + +`graff acp` answers `initialize` at a boot phase boundary. (a) `step` is the +decision `src/acp_early.zig` consults; the system around it (shared stdin, +protocol stdout, dispatch log) is checked over every boot trace up to a bound. +(b) The response cube: the early answer + loop-start replay leave the same +bytes and latches as the late path in every cell. + +`MUTANTS` are broken designs `check_properties` must reject. +""" + +from __future__ import annotations + +import json +from itertools import product + +STATES = ("booting", "answered", "passed", "loop") +LINES = ("none", "incomplete", "initialize", "other") +ACTIONS = ("none", "answer", "replay") +EVENTS: tuple[tuple, ...] = tuple(("boundary", acp, line) for acp in (False, True) for line in LINES) + (("loop_start",),) + +MUTANTS = ( + "answer-early-and-again-in-loop", + "replay-emits-response", + "consume-other-first-line", + "early-can-load-false", + "skip-replay", +) + + +def step(s: str, e: tuple, mutant: str | None = None) -> tuple[str, str]: + if e[0] == "boundary": + _, acp, line = e + if s != "booting": + return s, "none" + if not acp: + return "passed", "none" + if line in ("none", "incomplete"): + return "booting", "none" + if line == "initialize": + return "answered", "answer" + if mutant == "consume-other-first-line": + return "answered", "answer" + return "passed", "none" + if e[0] == "loop_start": + if s == "answered" and mutant != "skip-replay": + return "loop", "replay" + return "loop", "none" + raise ValueError(f"unknown event {e!r}") + + +# ── (a) the protocol: Sys over a boot trace ─────────────────────────────── + + +def classify(sys: dict) -> str: + if not sys["avail"]: + return "incomplete" + if not sys["input"]: + return "none" + return "initialize" if sys["input"][0][0] == "init" else "other" + + +def answer_of(m: tuple) -> tuple: + return ("resp", m[1]) + + +def start(inp: list) -> dict: + return {"st": "booting", "avail": False, "input": list(inp), "stash": None, "out": [], "disp": []} + + +def boot_step(acp: bool, s: dict, ev: str, mutant: str | None) -> dict: + s = dict(s, input=list(s["input"]), out=list(s["out"]), disp=list(s["disp"])) + if ev == "arrive": + s["avail"] = True + return s + st, act = step(s["st"], ("boundary", acp, classify(s)), mutant) + s["st"] = st + if act == "answer" and s["input"]: + m = s["input"].pop(0) + s["stash"] = m + s["out"].append(answer_of(m)) + return s + + +def loop_start(s: dict, mutant: str | None) -> dict: + s = dict(s, input=list(s["input"]), out=list(s["out"]), disp=list(s["disp"])) + st, act = step(s["st"], ("loop_start",), mutant) + s["st"] = st + if act == "replay" and s["stash"] is not None: + m = s["stash"] + s["stash"] = None + if mutant == "answer-early-and-again-in-loop": + s["input"].insert(0, m) # handed back to the queue: the loop answers it again + else: + s["disp"].append(m) + if mutant == "replay-emits-response": + s["out"].append(answer_of(m)) + return s + + +def drain(s: dict) -> dict: + s = dict(s, out=list(s["out"]), disp=list(s["disp"])) + for m in s["input"]: + s["disp"].append(m) + s["out"].append(answer_of(m)) + s["input"] = [] + return s + + +def run_early(acp: bool, inp: list, trace: tuple, mutant: str | None = None) -> tuple[dict, dict, dict]: + s = start(inp) + for ev in trace: + s = boot_step(acp, s, ev, mutant) + booted = s + started = loop_start(s, mutant) + return booted, started, drain(started) + + +def run_late(inp: list) -> dict: + return drain(loop_start(start(inp), None)) + + +def inputs(max_len: int = 3) -> list[list]: + out = [] + for n in range(max_len + 1): + for kinds in product(("init", "other"), repeat=n): + out.append([(k, i) for i, k in enumerate(kinds)]) + return out + + +def traces(max_len: int = 5) -> list[tuple]: + out = [] + for n in range(max_len + 1): + out.extend(product(("arrive", "boundary"), repeat=n)) + return out + + +def check_protocol(mutant: str | None = None) -> int: + n = 0 + for acp, inp, tr in product((False, True), inputs(), traces()): + n += 1 + booted, started, early = run_early(acp, inp, tr, mutant) + late = run_late(inp) + where = f"acp={acp} input={inp} trace={tr}" + if early["out"] != late["out"]: + raise ValueError(f"early_out_eq_late: {where}: {early['out']} != {late['out']}") + if early["disp"] != late["disp"]: + raise ValueError(f"early_disp_eq_late: {where}: {early['disp']} != {late['disp']}") + if started["out"] != booted["out"]: + raise ValueError(f"replay_silent: {where}") + if len(booted["out"]) > 1 or booted["disp"]: + raise ValueError(f"boot_writes_at_most_one: {where}") + if inp and inp[0][0] == "init" and early["out"].count(("resp", 0)) != 1: + raise ValueError(f"initialize_answered_once: {where}") + if inp and inp[0][0] == "other" and (booted["input"] != inp or booted["out"]): + raise ValueError(f"other_first_untouched: {where}") + return n + + +def check_step_table(mutant: str | None = None) -> int: + n = 0 + for s, e in product(STATES, EVENTS): + n += 1 + nxt, act = step(s, e, mutant) + if (act == "answer") != (s == "booting" and e == ("boundary", True, "initialize")): + raise ValueError(f"step_answer_iff: {s} {e}") + if (act == "replay") != (s == "answered" and e == ("loop_start",)): + raise ValueError(f"step_replay_iff: {s} {e}") + if e == ("loop_start",) and nxt != "loop": + raise ValueError(f"loop_start_reaches_loop: {s}") + if s == "loop" and (nxt, act) != ("loop", "none"): + raise ValueError(f"loop_absorbing: {e}") + return n + + +def step_rows() -> list[dict]: + rows = [] + for s, e in product(STATES, EVENTS): + nxt, act = step(s, e) + ev = {"kind": e[0]} if e[0] == "loop_start" else {"kind": "boundary", "acp": e[1], "line": e[2]} + rows.append({"state": s, "event": ev, "next": nxt, "action": act}) + return rows + + +# ── (b) the response cube ────────────────────────────────────────────────── + +PVS = ("missing", 0, 1, 2, 9) +SUBS = ("absent", "object", "null") +BGS = ("absent", "true", "false") +SEEDS = (0, 1, 0x5EED) +BOOT_LATCH = {"v2": False, "seed": 0, "t3": False, "elicit": False, "compaction": False, "subagents": False, "bg": False} + + +def is_v2(c: dict) -> bool: + return c["gate"] and c["pv"] != "missing" and c["pv"] >= 2 + + +def resp(c: dict, can_load: bool) -> dict: + v2 = is_v2(c) + return {"protocol_version": 2 if v2 else 1, "v2_shape": v2, "can_load": can_load, "auth_terminal": (not v2) or c["extras"]} + + +def latch(prev: dict, c: dict, seed: int) -> dict: + v2 = is_v2(c) + return { + "v2": v2, + "seed": seed if v2 else prev["seed"], + "t3": c["extras"], + "elicit": c["extras"], + "compaction": c["extras"] or v2, + "subagents": (not v2) and c["sub"] == "object", + "bg": c["bg"] == "true", + } + + +def late(c: dict, seed: int) -> tuple[dict, dict]: + return latch(BOOT_LATCH, c, seed), resp(c, c["can_load"]) + + +def early(c: dict, seed: int, mutant: str | None = None) -> tuple[dict, dict]: + scratch = latch(BOOT_LATCH, c, 0) + can_load = False if mutant == "early-can-load-false" else c["can_load"] + out = resp(c, can_load) + if mutant == "skip-replay": + # The scratch dispatch's fields are dropped; the live one stays fresh. + return dict(scratch, subagents=False, bg=False), out + return latch(scratch, c, seed), out + + +def request(c: dict, idx: int) -> str: + caps: dict = {"fs": {}} + if c["sub"] == "object": + caps["subagents"] = {} + elif c["sub"] == "null": + caps["subagents"] = None + if c["bg"] != "absent": + caps["_meta"] = {"graff/backgroundSubagents": c["bg"] == "true"} + params: dict = {} + if c["pv"] != "missing": + params["protocolVersion"] = c["pv"] + if c["extras"]: + params["clientInfo"] = {"name": "t3-code", "version": "1"} + caps["elicitation"] = {"form": {}} + caps["session"] = {"compaction": True} + params["capabilities"] = {"auth": {"terminal": {}}} + params["clientCapabilities"] = caps + return json.dumps({"jsonrpc": "2.0", "id": idx, "method": "initialize", "params": params}, separators=(",", ":")) + + +def cells() -> list[dict]: + rows = [] + for pv, gate, sub, bg, can_load, extras in product(PVS, (False, True), SUBS, BGS, (False, True), (False, True)): + c = {"pv": pv, "gate": gate, "sub": sub, "bg": bg, "can_load": can_load, "extras": extras} + lat, out = late(c, SEEDS[-1]) + rows.append(dict( + c, + id=f"pv{pv}.g{int(gate)}.sub{sub}.bg{bg}.cl{int(can_load)}.x{int(extras)}", + request=request(c, len(rows)), + response=out, + latches={k: v for k, v in lat.items() if k != "seed"}, + )) + return rows + + +def check_cube(mutant: str | None = None) -> int: + n = 0 + for c in cells(): + n += 1 + for seed in SEEDS: + if early(c, seed, mutant) != late(c, seed): + raise ValueError(f"cube_early_eq_late: {c['id']} seed={seed}") + lat = late(c, 7)[0] + if lat["v2"] and lat["subagents"]: + raise ValueError(f"cube_v2_no_subagents: {c['id']}") + v2_cells = sum(1 for c in cells() if is_v2(c)) + if v2_cells != 72: + raise ValueError(f"cube_v2_cells: {v2_cells}") + return n + + +def _check(mutant: str | None) -> int: + return check_step_table(mutant) + check_protocol(mutant) + check_cube(mutant) + + +def check_properties() -> int: + """Cells checked (step table + cube); every mutant must be rejected.""" + _check(None) + for m in MUTANTS: + try: + _check(m) + except ValueError: + continue + raise ValueError(f"mutant survived: {m}") + return len(STATES) * len(EVENTS) + len(cells()) + + +def payload() -> dict: + return { + "kernel": "acp_early_init", + "version": 1, + "universe": {"states": list(STATES), "lines": list(LINES), "actions": list(ACTIONS)}, + "mutants_rejected": list(MUTANTS), + "step": step_rows(), + "cases": cells(), + } diff --git a/src/acp.zig b/src/acp.zig index 7edae59cf..73aa89cda 100644 --- a/src/acp.zig +++ b/src/acp.zig @@ -257,17 +257,22 @@ pub fn runAcpCommand(gpa: Allocator, io: Io, environ_map: anytype, root: *agent_ var line_arena_state = std.heap.ArenaAllocator.init(gpa); defer line_arena_state.deinit(); var handshake: @import("acp_handshake_log.zig").Log = .{ .process_started = @import("startup_timing.zig").g_started }; + var replay_buf: [256]u8 = undefined; + var replay_sink: Io.Writer.Discarding = .init(&replay_buf); while (true) { defer @import("agent_request_scratch.zig").reset(&line_arena_state); const line_arena = line_arena_state.allocator(); - const event = (inbox.wait(line_arena) catch break) orelse break; + // An `initialize` answered during boot runs through the same handler + // first, its response discarded, so `d` and the latches match (acp_early.zig). + const replay = @import("acp_early.zig").loopStart(); + const event: @import("acp_inbox.zig").Inbox.Event = if (replay) |line| .{ .line = line } else (inbox.wait(line_arena) catch break) orelse break; switch (event) { .tick => @import("acp_idle.zig").maybeWake(&d, line_arena, wire, io, root.session_name) catch |err| { std.debug.print("acp: idle wake failed: {t}\n", .{err}); }, .line => |line| { const phase = handshake.before(io, line); // #1837 - handleLine(&d, line_arena, wire, line) catch |err| { + handleLine(&d, line_arena, if (replay != null) &replay_sink.writer else wire, line) catch |err| { std.debug.print("acp: dispatch failed: {t}\n", .{err}); break; }; diff --git a/src/acp_early.zig b/src/acp_early.zig new file mode 100644 index 000000000..7b945dfa7 --- /dev/null +++ b/src/acp_early.zig @@ -0,0 +1,203 @@ +//! `graff acp` answers the client's `initialize` during boot instead of after +//! the root Agent exists (draft ADR acp-early-initialize). Single-threaded: at +//! each boot phase boundary the main thread asks stdin, without blocking, +//! whether a complete first line is buffered. If it is an `initialize` +//! request, the line is taken off the ONE stdin reader the dispatch loop later +//! reads, answered through the engine's own `handleLine`, and kept. At loop +//! start that line is dispatched again through the normal handler with its +//! output discarded, so the live `Dispatch` and the process latches are rebuilt +//! by the code that builds them on the late path. +//! +//! `step` is the decision the Lean kernel `Graff.AcpEarlyInit` models; nothing +//! here decides without it. A first line that is incomplete, not `initialize`, +//! or larger than the stdin buffer is left in place for the loop. +const std = @import("std"); +const builtin = @import("builtin"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const engine = @import("acp_engine.zig"); +const proto = @import("acp_protocol.zig"); +const startup_timing = @import("startup_timing.zig"); + +// ── the kernel: lean-proofs/Graff/AcpEarlyInit.lean `step` ───────────────── + +pub const State = enum { booting, answered, passed, loop }; +/// What the shared stdin buffer holds at a check. `none`/`incomplete`: no +/// complete first line yet; `initialize`: a complete `initialize` request +/// with an id; `other`: any other complete first line. +pub const Line = enum { none, incomplete, initialize, other }; +/// `acp` is ACP mode with a clean protocol stdout; false declines for good. +pub const Event = union(enum) { boundary: struct { acp: bool, line: Line }, loop_start }; +pub const Action = enum { none, answer, replay }; +pub const Step = struct { next: State, action: Action }; + +pub fn step(s: State, e: Event) Step { + return switch (e) { + .boundary => |b| switch (s) { + .booting => if (!b.acp) .{ .next = .passed, .action = .none } else switch (b.line) { + .none, .incomplete => .{ .next = .booting, .action = .none }, + .initialize => .{ .next = .answered, .action = .answer }, + .other => .{ .next = .passed, .action = .none }, + }, + .answered, .passed, .loop => .{ .next = s, .action = .none }, + }, + .loop_start => .{ .next = .loop, .action = if (s == .answered) .replay else .none }, + }; +} + +// ── the response ─────────────────────────────────────────────────────────── + +/// What the live CLI dispatch advertises as `can_load`: `acp_session_load. +/// configure` always installs a loader (pinned by a test below). +pub const live_can_load = true; + +fn noTurn(_: *anyopaque, _: Allocator, _: []const u8) anyerror![]const u8 { + return ""; +} + +fn noLoad(_: *anyopaque, _: Allocator, _: *Io.Writer, _: proto.Request) anyerror!void {} + +/// The `initialize` answer, written by the engine's `handleLine` on a fresh +/// dispatch whose only setting the response reads (`load_session != null`) +/// matches the live one. The process latches it sets are overwritten by the +/// replay at loop start (last writer wins, ADR 0206 latch). +pub fn respond(gpa: Allocator, w: *Io.Writer, line: []const u8, can_load: bool) void { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + var scratch: engine.Dispatch = .{ .turn = noTurn, .ctx = undefined, .load_session = if (can_load) noLoad else null }; + engine.implementation_version = @import("build_options").version; + engine.handleLine(&scratch, arena.allocator(), w, line) catch return; + w.flush() catch {}; +} + +// ── the runtime ──────────────────────────────────────────────────────────── + +pub const Early = struct { + gpa: Allocator, + in: *Io.Reader, + out: *Io.Writer, + acp: bool, + /// Polled for readiness; null reads only what is already buffered. + fd: ?Io.File.Handle = null, + can_load: bool = live_can_load, + state: State = .booting, + /// The consumed `initialize`, owned until the loop has dispatched it. + stash: ?[]u8 = null, + handed: ?[]u8 = null, + + const max_reads_per_check = 8; + + /// One phase boundary: classify the first line, then do what `step` says. + pub fn boundary(self: *Early) bool { + if (self.state != .booting) return false; + const ready = self.acp and self.out.buffered().len == 0; + var copy: ?[]u8 = null; + const line: Line = if (ready) self.probe(©) else .none; + const s = step(self.state, .{ .boundary = .{ .acp = ready, .line = line } }); + self.state = s.next; + if (s.action != .answer) { + if (copy) |c| self.gpa.free(c); + return false; + } + const taken = copy.?; + self.in.toss(taken.len + 1); + self.stash = taken; + respond(self.gpa, self.out, taken, self.can_load); + return true; + } + + /// Loop start: the line to dispatch with its output discarded, or null. + /// A slice handed out stays valid until the next call. + pub fn loopStart(self: *Early) ?[]const u8 { + if (self.handed) |h| self.gpa.free(h); + self.handed = null; + const s = step(self.state, .loop_start); + self.state = s.next; + if (s.action != .replay) return null; + self.handed = self.stash; + self.stash = null; + return self.handed; + } + + /// Never blocks: reads only while poll says stdin is readable, and never + /// past a full buffer. `copy` receives the line only for `.initialize`. + fn probe(self: *Early, copy: *?[]u8) Line { + const in = self.in; + var reads: usize = 0; + while (true) { + if (std.mem.indexOfScalar(u8, in.buffered(), '\n')) |n| return self.classify(in.buffered()[0..n], copy); + if (in.bufferedLen() == in.buffer.len) return .incomplete; + if (reads == max_reads_per_check or !self.readable()) break; + in.fillMore() catch break; + reads += 1; + } + return if (in.bufferedLen() == 0) .none else .incomplete; + } + + fn classify(self: *Early, line: []const u8, copy: *?[]u8) Line { + var arena = std.heap.ArenaAllocator.init(self.gpa); + defer arena.deinit(); + const req = proto.parseRequest(arena.allocator(), line) orelse return .other; + if (!std.mem.eql(u8, req.method, "initialize") or req.id == null) return .other; + // Out of memory: not consumed, so the loop still answers it. + copy.* = self.gpa.dupe(u8, line) catch return .incomplete; + return .initialize; + } + + fn readable(self: *Early) bool { + if (comptime builtin.os.tag == .windows or builtin.os.tag == .wasi) return false; + const fd = self.fd orelse return false; + var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }}; + const n = std.posix.poll(&fds, 0) catch return false; + return n > 0 and fds[0].revents != 0; + } +}; + +// ── process wiring (main.zig, acp.zig) ───────────────────────────────────── + +var stdin_buf: [64 * 1024]u8 = undefined; +var stdin_reader: Io.File.Reader = undefined; +var g: ?Early = null; + +pub fn isAcp(positionals: []const []const u8) bool { + return (comptime @import("build_options").acp) and positionals.len > 0 and std.mem.eql(u8, positionals[0], "acp"); +} + +/// Creates the process stdin reader (every frontend reads this one) and runs +/// the first check. Outside ACP the first `step` declines without reading. +pub fn arm(io: Io, gpa: Allocator, out: *Io.Writer, positionals: []const []const u8) *Io.Reader { + stdin_reader = Io.File.stdin().reader(io, &stdin_buf); + g = .{ .gpa = gpa, .in = &stdin_reader.interface, .out = out, .acp = isAcp(positionals), .fd = Io.File.stdin().handle }; + _ = g.?.boundary(); + return &stdin_reader.interface; +} + +/// A boundary for boot code outside main.zig's marks (a long phase can call +/// it between its steps). No-op before `arm` and once `step` has decided. +pub fn check() void { + if (g) |*e| _ = e.boundary(); +} + +/// The ACP loop's first-iteration hook; null outside a process that armed. +pub fn loopStart() ?[]const u8 { + if (g) |*e| return e.loopStart(); + return null; +} + +/// The boot tracker with a check at every phase boundary. +pub const Boot = struct { + inner: startup_timing.Tracker, + + pub fn mark(self: *Boot, io: Io, label: []const u8) void { + self.inner.mark(io, label); + if (g) |*e| if (e.boundary() and self.inner.debug) std.debug.print("[boot] acp initialize answered after {s}\n", .{label}); + } + + pub fn attach(self: *Boot, tracer: anytype) void { + self.inner.attach(tracer); + } +}; + +test { + _ = @import("acp_early_tests.zig"); +} diff --git a/src/acp_early_tests.zig b/src/acp_early_tests.zig new file mode 100644 index 000000000..935a97c02 --- /dev/null +++ b/src/acp_early_tests.zig @@ -0,0 +1,260 @@ +//! Tests for acp_early.zig: the early `initialize` answer against the late +//! dispatch path, the shared stdin buffer, and the pre-auth path. +const std = @import("std"); +const builtin = @import("builtin"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const testing = std.testing; +const early = @import("acp_early.zig"); +const engine = @import("acp_engine.zig"); +const v2 = @import("acp_v2.zig"); + +/// Everything handling `initialize` writes: the dispatch fields and the +/// process latches (v2 wire + message-id seed, T3, elicitation, compaction). +pub const Latches = struct { + subagents: bool, + background_subagents: bool, + v2: bool, + v2_seed: [24]u8, + t3: bool, + elicit: @TypeOf(@import("acp_elicit.zig").mode), + compaction: bool, + + pub fn of(d: *const engine.Dispatch) Latches { + var mint_buf: [48]u8 = undefined; + const id = v2.mint(&mint_buf, "x"); // msg_x__; the counter is not a latch + var it = std.mem.splitScalar(u8, id, '_'); + _ = it.next(); + _ = it.next(); + var seed: [24]u8 = @splat(0); + const s = it.next() orelse ""; + @memcpy(seed[0..@min(s.len, seed.len)], s[0..@min(s.len, seed.len)]); + return .{ + .subagents = d.subagents, + .background_subagents = d.background_subagents, + .v2 = v2.on(), + .v2_seed = seed, + .t3 = @import("acp_t3.zig").active(), + .elicit = @import("acp_elicit.zig").mode, + .compaction = @import("acp_compaction.zig").enabled(), + }; + } +}; + +fn noTurn(_: *anyopaque, _: Allocator, _: []const u8) anyerror![]const u8 { + return ""; +} + +fn noLoad(_: *anyopaque, _: Allocator, _: *Io.Writer, _: @import("acp_protocol.zig").Request) anyerror!void {} + +/// Reset every latch to the boot value so cells do not leak into each other. +pub fn resetLatches() void { + v2.negotiate(null, 0); + @import("acp_t3.zig").note(null); + @import("acp_elicit.zig").configure(null); + @import("acp_compaction.zig").noteInitialize(null); +} + +pub const Outcome = struct { bytes: []const u8, latches: Latches }; + +/// The late path: the live loop dispatches the line on a dispatch seeded `seed`. +pub fn late(arena: Allocator, line: []const u8, can_load: bool, seed: u64) !Outcome { + resetLatches(); + var aw: Io.Writer.Allocating = .init(arena); + var d: engine.Dispatch = .{ .turn = noTurn, .ctx = undefined, .seed = seed, .load_session = if (can_load) noLoad else null }; + engine.implementation_version = @import("build_options").version; + try engine.handleLine(&d, arena, &aw.writer, line); + return .{ .bytes = aw.writer.buffered(), .latches = .of(&d) }; +} + +/// The early path: a boundary check answers from the shared reader, then the +/// loop-start replay dispatches the kept line on the live dispatch, output discarded. +pub fn earlyThenReplay(arena: Allocator, line: []const u8, can_load: bool, seed: u64) !Outcome { + resetLatches(); + const input = try std.mem.concat(arena, u8, &.{ line, "\n" }); + var in: Io.Reader = .fixed(input); + var aw: Io.Writer.Allocating = .init(arena); + var e: early.Early = .{ .gpa = testing.allocator, .in = &in, .out = &aw.writer, .acp = true, .can_load = can_load }; + if (!e.boundary()) return error.NotAnsweredEarly; + try testing.expectEqual(@as(usize, 0), in.bufferedLen()); + var d: engine.Dispatch = .{ .turn = noTurn, .ctx = undefined, .seed = seed, .load_session = if (can_load) noLoad else null }; + const replay = e.loopStart() orelse return error.NoReplay; + var sink_buf: [64]u8 = undefined; + var sink: Io.Writer.Discarding = .init(&sink_buf); + try engine.handleLine(&d, arena, &sink.writer, replay); + try testing.expect(e.loopStart() == null); // handed exactly once; frees it + return .{ .bytes = aw.writer.buffered(), .latches = .of(&d) }; +} + +test "acp_early: early answer is byte-identical to the late one and leaves the same state" { + var state = std.heap.ArenaAllocator.init(testing.allocator); + defer state.deinit(); + const a = state.allocator(); + defer resetLatches(); + const saved = v2.gate; + defer v2.gate = saved; + for ([_]?bool{ false, true }) |gate| { + v2.gate = gate; + for ([_][]const u8{ + "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1,\"clientCapabilities\":{\"fs\":{}}}}", + "{\"jsonrpc\":\"2.0\",\"id\":\"a\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":2,\"clientInfo\":{\"name\":\"t3-code\"},\"clientCapabilities\":{\"subagents\":{},\"_meta\":{\"graff/backgroundSubagents\":true},\"elicitation\":{\"form\":{}},\"session\":{\"compaction\":true}}}}", + "{\"id\":3,\"method\":\"initialize\"}", + }) |line| for ([_]bool{ false, true }) |can_load| { + const l = try late(a, line, can_load, 0x5eed); + const e = try earlyThenReplay(a, line, can_load, 0x5eed); + try testing.expectEqualStrings(l.bytes, e.bytes); + try testing.expectEqual(l.latches, e.latches); + }; + } +} + +test "acp_early: a non-initialize first line is left untouched for the loop" { + for ([_][]const u8{ + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"session/new\",\"params\":{}}\n{\"id\":2,\"method\":\"initialize\"}\n", + "{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"params\":{}}\n", // a notification is not answered + "not json\n", + "\n{\"id\":1,\"method\":\"initialize\"}\n", + }) |input| { + var in: Io.Reader = .fixed(input); + var aw: Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); + var e: early.Early = .{ .gpa = testing.allocator, .in = &in, .out = &aw.writer, .acp = true }; + try testing.expect(!e.boundary()); + try testing.expect(!e.boundary()); // passed: never looks again + try testing.expectEqual(early.State.passed, e.state); + try testing.expectEqualStrings(input, in.buffered()); + try testing.expectEqual(@as(usize, 0), aw.writer.buffered().len); + try testing.expect(e.loopStart() == null); + } +} + +test "acp_early: a first line that fills the buffer, or a dirty stdout, is declined" { + var big: [64]u8 = @splat('x'); + var in: Io.Reader = .fixed(&big); // no newline anywhere in a full buffer + var aw: Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); + var e: early.Early = .{ .gpa = testing.allocator, .in = &in, .out = &aw.writer, .acp = true }; + try testing.expect(!e.boundary()); + try testing.expectEqual(early.State.booting, e.state); + try testing.expectEqual(big.len, in.bufferedLen()); + + var in2: Io.Reader = .fixed("{\"id\":1,\"method\":\"initialize\"}\n"); + try aw.writer.writeAll("junk"); + var dirty: early.Early = .{ .gpa = testing.allocator, .in = &in2, .out = &aw.writer, .acp = true }; + try testing.expect(!dirty.boundary()); + try testing.expectEqual(early.State.passed, dirty.state); + try testing.expectEqualStrings("junk", aw.writer.buffered()); +} + +fn pipePair() ![2]std.posix.fd_t { + if (comptime builtin.os.tag == .windows or builtin.os.tag == .wasi) return error.SkipZigTest; + var fds: [2]std.posix.fd_t = undefined; + if (std.c.pipe(&fds) != 0) return error.SkipZigTest; + return fds; +} + +fn writeFd(fd: std.posix.fd_t, bytes: []const u8) !void { + if (std.c.write(fd, bytes.ptr, bytes.len) != @as(isize, @intCast(bytes.len))) return error.ShortWrite; +} + +test "acp_early: a partial first line is not consumed and boot does not wait for the rest" { + resetLatches(); + defer resetLatches(); + const fds = try pipePair(); + defer _ = std.c.close(fds[0]); + defer _ = std.c.close(fds[1]); + var buf: [4096]u8 = undefined; + var reader = (Io.File{ .handle = fds[0], .flags = .{ .nonblocking = false } }).reader(testing.io, &buf); + var aw: Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); + var e: early.Early = .{ .gpa = testing.allocator, .in = &reader.interface, .out = &aw.writer, .acp = true, .fd = fds[0] }; + try testing.expect(!e.boundary()); // nothing written yet: returns at once + try testing.expectEqual(early.State.booting, e.state); + try writeFd(fds[1], "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initia"); + try testing.expect(!e.boundary()); + try testing.expectEqual(early.State.booting, e.state); + try testing.expectEqualStrings("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initia", reader.interface.buffered()); + try testing.expectEqual(@as(usize, 0), aw.writer.buffered().len); + try writeFd(fds[1], "lize\"}\n{\"id\":2,\"method\":\"session/new\"}\n{\"id\":3,"); + try testing.expect(e.boundary()); + try testing.expectEqual(early.State.answered, e.state); + try testing.expect(std.mem.indexOf(u8, aw.writer.buffered(), "\"id\":1,\"result\"") != null); + // The bytes after the answered line stay in the one shared buffer, in order. + try testing.expectEqualStrings("{\"id\":2,\"method\":\"session/new\"}\n{\"id\":3,", reader.interface.buffered()); + const answered = aw.writer.buffered().len; + try testing.expect(!e.boundary()); // exactly once + try testing.expectEqual(answered, aw.writer.buffered().len); + try testing.expectEqualStrings("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"}", e.loopStart().?); + try testing.expect(e.loopStart() == null); + try testing.expectEqual(answered, aw.writer.buffered().len); +} + +test "acp_early: outside ACP the first check declines without reading stdin" { + const fds = try pipePair(); + defer _ = std.c.close(fds[0]); + defer _ = std.c.close(fds[1]); + try writeFd(fds[1], "{\"id\":1,\"method\":\"initialize\"}\n"); + var buf: [256]u8 = undefined; + var reader = (Io.File{ .handle = fds[0], .flags = .{ .nonblocking = false } }).reader(testing.io, &buf); + var aw: Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); + var e: early.Early = .{ .gpa = testing.allocator, .in = &reader.interface, .out = &aw.writer, .acp = false, .fd = fds[0] }; + try testing.expect(!e.boundary()); + try testing.expectEqual(early.State.passed, e.state); + try testing.expectEqual(@as(usize, 0), reader.interface.bufferedLen()); + try testing.expect(e.loopStart() == null); +} + +test "acp_early: live_can_load is what acp_session_load.configure installs" { + var state = std.heap.ArenaAllocator.init(testing.allocator); + defer state.deinit(); + const a = state.allocator(); + var root: @import("agent.zig").Agent = .{ + .gpa = testing.allocator, + .arena = a, + .io = testing.io, + .client = undefined, + .provider = .{ .id = "codex", .kind = .responses, .auth = .bearer, .url = "", .api_key = "", .model = "m", .context = 1 }, + .messages = std.json.Array.init(a), + .sub = false, + .label = "test", + .out = null, + }; + var live: @import("acp_live_turn.zig").LiveTurn = .{ .root = &root, .keys = undefined, .out = undefined }; + var d: engine.Dispatch = .{ .turn = noTurn, .ctx = &live }; + @import("acp_session_load.zig").configure(&d, &live); + try testing.expectEqual(early.live_can_load, d.load_session != null); +} + +test "acp_early: the pre-auth path still answers initialize like the engine with can_load false" { + var state = std.heap.ArenaAllocator.init(testing.allocator); + defer state.deinit(); + const a = state.allocator(); + defer resetLatches(); + const line = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1}}"; + resetLatches(); + var pre: Io.Writer.Allocating = .init(a); + try engine.handlePreAuthLine(a, &pre.writer, line); + var direct: Io.Writer.Allocating = .init(a); + resetLatches(); + early.respond(a, &direct.writer, line, false); + try testing.expectEqualStrings(pre.writer.buffered(), direct.writer.buffered()); + var gated: Io.Writer.Allocating = .init(a); + try engine.handlePreAuthLine(a, &gated.writer, "{\"id\":2,\"method\":\"session/new\"}"); + try testing.expect(std.mem.indexOf(u8, gated.writer.buffered(), "\"error\"") != null); +} + +test "acp_early: step answers only a booting initialize and replays only an answered one" { + const states = [_]early.State{ .booting, .answered, .passed, .loop }; + const lines = [_]early.Line{ .none, .incomplete, .initialize, .other }; + for (states) |s| { + for ([_]bool{ false, true }) |acp| for (lines) |l| { + const r = early.step(s, .{ .boundary = .{ .acp = acp, .line = l } }); + try testing.expectEqual(s == .booting and acp and l == .initialize, r.action == .answer); + try testing.expect(r.action != .replay); + }; + const r = early.step(s, .loop_start); + try testing.expectEqual(early.State.loop, r.next); + try testing.expectEqual(s == .answered, r.action == .replay); + } +} diff --git a/src/main.zig b/src/main.zig index abe49f4c5..4feef4a82 100644 --- a/src/main.zig +++ b/src/main.zig @@ -247,7 +247,7 @@ const session_run = @import("session_run.zig"); pub fn main(init: std.process.Init) !void { if ((comptime @import("build_options").acp) and init.environ_map.get("GRAFF_AGENT_OBSERVER") != null) return @import("acp_agents.zig").run(init); if (try @import("external_tui.zig").maybeRun(init)) return; - var boot = startup_timing.Tracker.init(startup_timing.shutdown_trace.arm(init.io, init.environ_map), init.environ_map.get("GRAFF_BOOT_DEBUG") != null); // #364: arm() passes io through, so the teardown below stamps phases without a line of its own + var boot: @import("acp_early.zig").Boot = .{ .inner = startup_timing.Tracker.init(startup_timing.shutdown_trace.arm(init.io, init.environ_map), init.environ_map.get("GRAFF_BOOT_DEBUG") != null) }; // #364: arm() passes io through, so the teardown below stamps phases without a line of its own; every mark is also an ACP early-initialize check const gpa = init.gpa; const io = init.io; defer @import("session_writer.zig").stop(); @@ -301,13 +301,11 @@ pub fn main(init: std.process.Init) !void { defer client_runtime.deinit(startup_timing.shutdown_trace.at(io, "ca-warm-await")); const client = &client_runtime.client; boot.mark(io, "CA warm scheduled"); - var stdin_buf: [64 * 1024]u8 = undefined; - var stdin_reader = Io.File.stdin().reader(io, &stdin_buf); - const in = &stdin_reader.interface; var stdout_buf: [4096]u8 = undefined; var stdout_writer = Io.File.stdout().writer(io, &stdout_buf); const out = &stdout_writer.interface; g_out = out; + const in = @import("acp_early.zig").arm(io, gpa, out, flags.positionals.items); // the one stdin reader; ACP may answer `initialize` from it before the root exists if (try session_start.runTitleCommand(io, gpa, arena, client, default_provider, out, flags, &invocation_budget)) return; // Generate identity before opening either JSONL. The score channel and both // files share this run id; session_id is a separate runtime correlation id. @@ -591,7 +589,7 @@ test { // ── Unit tests (`zig build test`): pull in tests from imported modu _ = .{ @import("presence_accord.zig"), @import("peer_idle.zig"), @import("acp_idle.zig"), @import("presence_chan.zig"), @import("presence_record.zig"), @import("daddy.zig"), @import("peer_inbox_content_test.zig"), @import("peer_inbox_failure_test.zig"), @import("peer_inbox_storage_test.zig"), @import("peer_inbox_sequence_test.zig"), @import("peer_inbox_snapshot_test.zig") }; _ = .{ @import("acp_agents.zig"), @import("repo_context_tests.zig"), @import("session_select.zig") }; _ = .{ @import("subagent_activity.zig"), @import("subagent_steer.zig"), @import("subagent_interactive.zig"), @import("subagent_recovery.zig"), @import("subagent_retained.zig"), @import("subagent_history.zig"), @import("subagent_history_copy.zig"), @import("subagent_history_tests.zig"), @import("subagent_run_tests.zig"), @import("workflow_synthesis.zig") }; - _ = .{ @import("acp_agent_activity.zig"), @import("acp_subagent_live.zig"), @import("acp_line_writer.zig") }; + _ = .{ @import("acp_agent_activity.zig"), @import("acp_subagent_live.zig"), @import("acp_line_writer.zig"), @import("acp_early.zig") }; _ = .{ @import("acp_replay.zig"), @import("python_version.zig"), @import("jev_auto.zig"), @import("background_wait.zig"), @import("gateway_compact_tests.zig") }; _ = @import("read_image.zig"); _ = @import("image_orient.zig"); diff --git a/src/spec_acp_early_init_conformance.zig b/src/spec_acp_early_init_conformance.zig new file mode 100644 index 000000000..91d6a13c6 --- /dev/null +++ b/src/spec_acp_early_init_conformance.zig @@ -0,0 +1,95 @@ +//! Impl half of AcpEarlyInit: the fixture's step table drives the REAL +//! `acp_early.step`, and every cube cell's request runs through the real +//! engine twice — the late path (live dispatch) and the early path (boundary +//! answer from the shared reader, then the loop-start replay with its output +//! discarded). Bytes and latches must be identical, and match the model. + +const std = @import("std"); +const Value = std.json.Value; +const early = @import("acp_early.zig"); +const harness = @import("acp_early_tests.zig"); +const v2 = @import("acp_v2.zig"); + +const fixtures_json = @embedFile("spec_acp_early_init"); + +fn str(o: std.json.ObjectMap, key: []const u8) []const u8 { + return o.get(key).?.string; +} + +fn boolean(o: std.json.ObjectMap, key: []const u8) bool { + return o.get(key).?.bool; +} + +fn names(comptime E: type, list: []const Value) !void { + const fields = comptime std.meta.fieldNames(E); + try std.testing.expectEqual(fields.len, list.len); + for (fields, 0..) |name, i| try std.testing.expectEqualStrings(name, list[i].string); +} + +test "spec/acp_early_init: the step table is the Zig step" { + var state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer state.deinit(); + const root = try std.json.parseFromSliceLeaky(Value, state.allocator(), fixtures_json, .{}); + const universe = root.object.get("universe").?.object; + try names(early.State, universe.get("states").?.array.items); + try names(early.Line, universe.get("lines").?.array.items); + try names(early.Action, universe.get("actions").?.array.items); + const rows = root.object.get("step").?.array.items; + try std.testing.expectEqual(@as(usize, 36), rows.len); + for (rows) |row_v| { + const row = row_v.object; + const s = std.meta.stringToEnum(early.State, str(row, "state")).?; + const ev = row.get("event").?.object; + const e: early.Event = if (std.mem.eql(u8, str(ev, "kind"), "loop_start")) + .loop_start + else + .{ .boundary = .{ .acp = boolean(ev, "acp"), .line = std.meta.stringToEnum(early.Line, str(ev, "line")).? } }; + const got = early.step(s, e); + try std.testing.expectEqualStrings(str(row, "next"), @tagName(got.next)); + try std.testing.expectEqualStrings(str(row, "action"), @tagName(got.action)); + } +} + +test "spec/acp_early_init: early answer + replay equals the late path in every cube cell" { + var state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer state.deinit(); + const a = state.allocator(); + const root = try std.json.parseFromSliceLeaky(Value, a, fixtures_json, .{}); + const cases = root.object.get("cases").?.array.items; + try std.testing.expect(cases.len >= 360); + const saved = v2.gate; + defer v2.gate = saved; + defer harness.resetLatches(); + for (cases) |cell_v| { + var cell_arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer cell_arena.deinit(); + const ca = cell_arena.allocator(); + const cell = cell_v.object; + v2.gate = boolean(cell, "gate"); + const can_load = boolean(cell, "can_load"); + const line = str(cell, "request"); + const late = try harness.late(ca, line, can_load, 0x5eed); + const got = try harness.earlyThenReplay(ca, line, can_load, 0x5eed); + std.testing.expectEqualStrings(late.bytes, got.bytes) catch |err| { + std.debug.print("cell {s}\n", .{str(cell, "id")}); + return err; + }; + try std.testing.expectEqual(late.latches, got.latches); + + // Both match the model's response and latches. + const want = cell.get("response").?.object; + const resp = (try std.json.parseFromSliceLeaky(Value, ca, std.mem.trimEnd(u8, got.bytes, "\n"), .{})).object.get("result").?.object; + try std.testing.expectEqual(want.get("protocol_version").?.integer, resp.get("protocolVersion").?.integer); + try std.testing.expectEqual(boolean(want, "v2_shape"), resp.get("info") != null); + const usage = if (std.mem.indexOf(u8, got.bytes, "\"codegraff/usage\":true") != null) true else if (std.mem.indexOf(u8, got.bytes, "\"codegraff/usage\":false") != null) false else return error.NoUsageMarker; + try std.testing.expectEqual(boolean(want, "can_load"), usage); + try std.testing.expectEqual(boolean(want, "auth_terminal"), std.mem.indexOf(u8, got.bytes, "graff-login") != null); + const lat = cell.get("latches").?.object; + try std.testing.expectEqual(boolean(lat, "v2"), got.latches.v2); + try std.testing.expectEqual(boolean(lat, "t3"), got.latches.t3); + try std.testing.expectEqual(boolean(lat, "elicit"), got.latches.elicit != .none); + try std.testing.expectEqual(boolean(lat, "compaction"), got.latches.compaction); + try std.testing.expectEqual(boolean(lat, "subagents"), got.latches.subagents); + try std.testing.expectEqual(boolean(lat, "bg"), got.latches.background_subagents); + } +} diff --git a/src/tool_gates.zig b/src/tool_gates.zig index 757e405f8..6b59e3844 100644 --- a/src/tool_gates.zig +++ b/src/tool_gates.zig @@ -119,6 +119,7 @@ test { // the served catalogs' wire-compatibility guard (an unreferenced module' _ = @import("spec_prompt_cache_kernel_conformance.zig"); _ = @import("spec_prompt_prefix_conformance.zig"); _ = @import("spec_prompt_stable_conformance.zig"); + _ = @import("spec_acp_early_init_conformance.zig"); } test "#352: an optional tool is advertised only while its flag is set; other names are unaffected" { From 4ab14e593658a27add6efe6a05b69ed00faa80b9 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:00:50 +0800 Subject: [PATCH 06/21] docs/adr: number the startup ADRs 0336-0338; index BootPrewarm Assign 0336 (update-check cache), 0337 (boot prewarm) and 0338 (ACP early initialize) with index rows, list BootPrewarm in formal/README.md and its invariants in formal/change-evidence.md, and reuse catalog_bounded.privateThreaded from update_check_cache instead of a copy. Co-Authored-By: Codegraff --- ...=> 0336-the-repl-update-notice-reads-a-cache.md} | 2 +- ...-a-one-shot-prewarms-the-companion-handshake.md} | 4 ++-- ...d => 0338-acp-answers-initialize-during-boot.md} | 13 +++++++------ docs/adr/README.md | 3 +++ formal/README.md | 3 +++ formal/boot-prewarm.md | 2 +- formal/change-evidence.md | 4 ++++ src/acp_early.zig | 2 +- src/boot_prewarm.zig | 2 +- src/catalog_bounded.zig | 2 +- src/update_check_cache.zig | 10 +--------- 11 files changed, 25 insertions(+), 22 deletions(-) rename docs/adr/{DRAFT-update-check-stale-while-revalidate.md => 0336-the-repl-update-notice-reads-a-cache.md} (96%) rename docs/adr/{DRAFT-boot-prewarm.md => 0337-a-one-shot-prewarms-the-companion-handshake.md} (97%) rename docs/adr/{DRAFT-acp-early-initialize.md => 0338-acp-answers-initialize-during-boot.md} (88%) diff --git a/docs/adr/DRAFT-update-check-stale-while-revalidate.md b/docs/adr/0336-the-repl-update-notice-reads-a-cache.md similarity index 96% rename from docs/adr/DRAFT-update-check-stale-while-revalidate.md rename to docs/adr/0336-the-repl-update-notice-reads-a-cache.md index 0d430095a..551d4a107 100644 --- a/docs/adr/DRAFT-update-check-stale-while-revalidate.md +++ b/docs/adr/0336-the-repl-update-notice-reads-a-cache.md @@ -1,4 +1,4 @@ -# NNNN. The REPL update notice reads a cache; the network never holds the banner +# 0336. The REPL update notice reads a cache; the network never holds the banner Status: accepted 2026-11-12 diff --git a/docs/adr/DRAFT-boot-prewarm.md b/docs/adr/0337-a-one-shot-prewarms-the-companion-handshake.md similarity index 97% rename from docs/adr/DRAFT-boot-prewarm.md rename to docs/adr/0337-a-one-shot-prewarms-the-companion-handshake.md index 705fb88ec..8e3e2317b 100644 --- a/docs/adr/DRAFT-boot-prewarm.md +++ b/docs/adr/0337-a-one-shot-prewarms-the-companion-handshake.md @@ -1,6 +1,6 @@ -# NNNN. A one-shot prewarms the companion handshake; teardown ends retry pauses +# 0337. A one-shot prewarms the companion handshake; teardown ends retry pauses -Status: proposed (number assigned at merge) +Status: accepted 2026-11-12 ## Context diff --git a/docs/adr/DRAFT-acp-early-initialize.md b/docs/adr/0338-acp-answers-initialize-during-boot.md similarity index 88% rename from docs/adr/DRAFT-acp-early-initialize.md rename to docs/adr/0338-acp-answers-initialize-during-boot.md index f25d5f385..4eb0c0f7a 100644 --- a/docs/adr/DRAFT-acp-early-initialize.md +++ b/docs/adr/0338-acp-answers-initialize-during-boot.md @@ -1,6 +1,6 @@ -# DRAFT. ACP answers `initialize` during boot +# 0338. ACP answers `initialize` during boot -Status: proposed. The integrator assigns the number and adds the index row. +Status: accepted 2026-11-12 ## Context @@ -60,9 +60,10 @@ Differences from the late path: succeed and then sees EOF. Before this change it saw EOF alone. - The handshake log's `startup` phase still measures time to the loop. It times the replay, not when the client was answered. -- A request that arrives during root construction, the longest phase, is - answered at the `root agent` mark, only slightly before the loop would - have answered it. Boot code inside a long phase can call - `acp_early.check()` between its steps to narrow the gap. +- A request that arrives during root construction is answered at the + `root agent` mark, just before the loop would have answered it. With + worktree identity resolved in-process that phase takes about 2 ms, so + `buildRootAgent` has no checks of its own. If it grows long again, boot + code inside it can call `acp_early.check()` between its steps. - Windows has no `poll` on stdin. There, only bytes already buffered count, which in practice means the late path. diff --git a/docs/adr/README.md b/docs/adr/README.md index 288dea9ea..39b948f42 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -360,6 +360,9 @@ record only when you need the evidence or the edge cases. | [0333](0333-bots-are-folders-and-routines-need-a-tick.md) | A Bot is the folder `.graff/bots//` (job, `memory.md`, `log.md`, routines); a run is one headless turn with those appended to the system prompt. Routines fire only when `graff bot tick` or a client's `graff bot due --claim` runs: there is no daemon. Approvals stay the headless default unless the bot was created with `--yolo`; an event payload is data, not instructions. | | [0334](0334-bots-can-be-per-user-and-a-late-check-reports-what-it-missed.md) | `graff bot --user` is the same store in `~/.graff/bots`, with a `project` its runs work in, one `primary` bot and a client's `chat` id. `tick`/`due --claim` record the last check; with `--stale-after` an old slot is recorded as missed (`graff bot missed`) and not run late. In a client's chat a bot changes its store with `graff bot remember\|forget\|log`, not file edits. `graff bot state --json` is one call for everything a client draws. Bots are off until `graff bot enable` writes `~/.graff/bots/enabled` (`GRAFF_BOTS=1\|0` decides for one process); while off every other `graff bot` command refuses. | | [0335](0335-the-primary-bot-works-across-projects.md) | The primary bot is the user's main bot: one thread for every project. In a client's chat it starts each piece of work as its own chat where it belongs (Harness: `list_projects`, `create_chat`), ends its turn, and reports when the reply comes back; it asks only when the project is unclear, and leaves publishing, sending, paying and deleting to the user. Headless, it names the project and gives the command. Single-job bots are helpers it hands work to. All of it is prompt text; the store is unchanged. | +| [0336](0336-the-repl-update-notice-reads-a-cache.md) | The REPL banner's update notice is read from `~/.graff/update-check.json` and boot makes no network call; a missing, corrupt or 24 h-old cache is refreshed on a detached worker with a private Io that exit never joins, so a new release shows one launch late. `GRAFF_NO_UPDATE_CHECK` reads and starts nothing; `graff update` and `/version` still fetch live and write the cache. | +| [0337](0337-a-one-shot-prewarms-the-companion-handshake.md) | A lean `-p` that will build the empty registry starts the companion connect right after `runSubcommand` and joins it in `connectCompanion` where `addServer` ran, so the first request's catalog is byte-identical; a cwd, home or setting mismatch or an unadopted prewarm is cancelled, joined and torn down, and a quick failure connects inline. `Registry.deinit` sets `retry_abort` so exit never waits out a start's retry pause, and still joins it. Modelled in `formal/BootPrewarm.tla`. | +| [0338](0338-acp-answers-initialize-during-boot.md) | `graff acp` answers `initialize` at a boot phase boundary: a non-blocking check takes a complete first `initialize` off the one stdin reader, answers it through the engine's `handleLine`, and replays it silently at loop start so dispatch state is rebuilt by the late-path code; any other first line is left for the loop. Proved as `Graff.AcpEarlyInit`. | ## When to write one diff --git a/formal/README.md b/formal/README.md index ad29451bc..e82d4eff3 100644 --- a/formal/README.md +++ b/formal/README.md @@ -12,6 +12,9 @@ are difficult to reproduce with ordinary tests. workers, offered choices, and responses racing with cancellation. - [HTTP/2 leases](http2-leases.md): exclusive ownership, idle pooling, cancellation, shutdown, and reuse only after stream completion. +- [Boot prewarm](boot-prewarm.md): a one-shot's companion connect joined at + its one consumer, discarded on a cwd or setting mismatch, and teardown + ending a start's retry pause while still joining it. Each model has a normal configuration and a deliberately broken configuration. The negative control must fail the intended invariant. A syntax error, timeout, diff --git a/formal/boot-prewarm.md b/formal/boot-prewarm.md index f13d48cde..7f02b036a 100644 --- a/formal/boot-prewarm.md +++ b/formal/boot-prewarm.md @@ -3,7 +3,7 @@ `BootPrewarm.tla` is a finite model of two lifecycles that decide whether a worker can still use the process `Io` when boot or teardown moves on: -- the companion prewarm (`src/boot_prewarm.zig`, DRAFT-boot-prewarm ADR): a +- the companion prewarm (`src/boot_prewarm.zig`, ADR 0337): a worker connects `codedb-pro --mcp` before the registry exists, and the boot thread joins it at its one consumer, `connectCompanion`; - one registry start task's retry pause (`src/mcp_start.zig`): teardown must diff --git a/formal/change-evidence.md b/formal/change-evidence.md index 355ea76f9..e83bd1ad0 100644 --- a/formal/change-evidence.md +++ b/formal/change-evidence.md @@ -29,6 +29,10 @@ For each behavioral change, retain this chain: | A synchronous predecessor stops early admission | `NoLateAdmission` | `src/agent_async_tools.zig`: `async tools do not cross a synchronous predecessor or execute partial arguments` | | Owned workers retire before their storage is freed | Async cancellation/reset model | `src/agent_async_tools.zig`: `async tools reset cancels and joins workers before freeing owned state` | | Permission replies belong to their live transport and session | ACP permission extension | Client-side evidence moved with the desktop app to Harness | +| A companion prewarm is joined before its result is used | `JoinedBeforeUse` | `src/boot_prewarm.zig`: `boot prewarm: the adopted companion matches the inline addServer catalog byte for byte` | +| A prewarm started in another cwd is never adopted | `NoStaleAdoption` | `src/boot_prewarm.zig`: `boot prewarm: a cwd change between start and consumer discards it and the inline path runs` | +| No prewarm worker outlives teardown | `NoUseAfterTeardown` | `src/boot_prewarm.zig`: `boot prewarm: discard joins a pending handshake and tears it down` | +| Teardown ends a retry pause and the retry does not run | `NoRetryAfterAbort` | `src/mcp_start.zig`: `teardown with a pending transient retry returns in well under the pause` | These anchors connect intent and implementation tests. They do not establish that every modeled interleaving is exercised by a runtime test, or that the diff --git a/src/acp_early.zig b/src/acp_early.zig index 7b945dfa7..d764c5a9e 100644 --- a/src/acp_early.zig +++ b/src/acp_early.zig @@ -1,5 +1,5 @@ //! `graff acp` answers the client's `initialize` during boot instead of after -//! the root Agent exists (draft ADR acp-early-initialize). Single-threaded: at +//! the root Agent exists (ADR 0338). Single-threaded: at //! each boot phase boundary the main thread asks stdin, without blocking, //! whether a complete first line is buffered. If it is an `initialize` //! request, the line is taken off the ONE stdin reader the dispatch loop later diff --git a/src/boot_prewarm.zig b/src/boot_prewarm.zig index 2e75a9c37..46033011c 100644 --- a/src/boot_prewarm.zig +++ b/src/boot_prewarm.zig @@ -1,6 +1,6 @@ //! Boot prewarm: run the companion handshake (`codedb-pro --mcp`) while the //! rest of a one-shot boots, and join it where `connectCompanion` would have -//! called `addServer` (DRAFT-boot-prewarm ADR; formal/BootPrewarm.tla). +//! called `addServer` (ADR 0337; formal/BootPrewarm.tla). //! //! `-p` connects the companion synchronously (ADR 0230 defers only //! interactive and ACP sessions), so its spawn + handshake used to sit in diff --git a/src/catalog_bounded.zig b/src/catalog_bounded.zig index 9e9aceca7..df466e63b 100644 --- a/src/catalog_bounded.zig +++ b/src/catalog_bounded.zig @@ -77,7 +77,7 @@ const Task = struct { /// outlives the Io around it, so its restore can reinstate the default action /// another Io had replaced, and that Io's next cancellation SIGIO kills the /// process. The worker leaves the no-op handler installed instead. -fn privateThreaded() Io.Threaded { +pub fn privateThreaded() Io.Threaded { var threaded: Io.Threaded = .init(alloc, .{}); threaded.have_signal_handler = false; return threaded; diff --git a/src/update_check_cache.zig b/src/update_check_cache.zig index 6326cfae8..7645b9eda 100644 --- a/src/update_check_cache.zig +++ b/src/update_check_cache.zig @@ -138,7 +138,7 @@ const Task = struct { fn run(t: *Task) void { defer _ = live.fetchSub(1, .release); defer t.destroy(); - var threaded = privateThreaded(); + var threaded = @import("catalog_bounded.zig").privateThreaded(); // the worker may outlive every other Io defer threaded.deinit(); const io = threaded.io(); var arena_state = std.heap.ArenaAllocator.init(alloc); @@ -148,14 +148,6 @@ const Task = struct { } }; -/// Same as `catalog_bounded.privateThreaded`: the worker may outlive every -/// other Io, so it must not restore the SIGIO/SIGPIPE action on deinit. -fn privateThreaded() Io.Threaded { - var threaded: Io.Threaded = .init(alloc, .{}); - threaded.have_signal_handler = false; - return threaded; -} - /// Start a refresh nobody waits for. False when it could not start. fn detach(home: []const u8, running: []const u8, fetch: Fetch) bool { if (builtin.single_threaded or home.len == 0) return false; From d05aef64e392dbc2c8706633486dd842f820355e Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:18:29 +0800 Subject: [PATCH 07/21] scripts: tighten startup budgets to the integrated boot REPL and ACP boot no longer spawn git, so their git gates go to 0 in both variants; the REPL-ready and ACP-initialize ratio gates drop to about 2.5x the measured ratios, which also hold with every core busy. Co-Authored-By: Codegraff --- scripts/startup-budgets.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/startup-budgets.json b/scripts/startup-budgets.json index aa996729c..c0c3a15ce 100644 --- a/scripts/startup-budgets.json +++ b/scripts/startup-budgets.json @@ -1,35 +1,35 @@ { "schema": 1, - "_about": "Gates for scripts/bench-startup.py --budgets. Time gates are median / floor median (floor = compiled C hello), not absolute ms, so a slower or busier machine scales both sides. Ratio maxima are about 2.5x the baseline ratio so they hold under load; tighten them after an optimization lands. Spawn gates are exact counts from the PATH-shim pass and do not depend on load. 'target' is the planned value once the startup work lands; only 'max' is enforced.", + "_about": "Gates for scripts/bench-startup.py --budgets. Time gates are median / floor median (floor = compiled C hello), not absolute ms, so a slower or busier machine scales both sides. Ratio maxima are about 2.5x the measured ratio so they hold under load; tighten them after an optimization lands. Spawn gates are exact counts from the PATH-shim pass and do not depend on load. 'target' is the planned value once further startup work lands; only 'max' is enforced.", "floor": "hello_ms", "ratio_over_floor": { - "_baseline": "Ratios measured on the unchanged baseline, hermetic, non-git cwd: a_version 1.6, a_help 1.5, b ready 10.3, c request 3.7, c wall 5.0, d response 10.2, d no-key 10.3. The --git-repo ratios are similar.", + "_baseline": "Ratios before the startup work, hermetic, non-git cwd: a_version 1.6, a_help 1.5, b ready 10.3, c request 3.7, c wall 5.0, d response 10.2, d no-key 10.3. After in-process worktree identity and the early ACP initialize: b ready 3.8, b wall 4.2, d response 1.5 (1.4 / 3.0 / 3.4 with every core busy); --git-repo is similar. c and d no-key did not move in this benchmark.", "a_version.wall_ms": {"max": 4}, "a_help.wall_ms": {"max": 4}, - "b_repl_ready.ready_ms": {"max": 26, "target": 6}, - "b_repl_ready.wall_ms": {"max": 28}, + "b_repl_ready.ready_ms": {"max": 10}, + "b_repl_ready.wall_ms": {"max": 11}, "c_first_request.request_ms": {"max": 10}, "c_first_request.wall_ms": {"max": 13}, - "d_acp_initialize.response_ms": {"max": 26, "target": 4}, + "d_acp_initialize.response_ms": {"max": 5}, "d_acp_initialize_nokey.response_ms": {"max": 26} }, "spawns": { "default": { - "_tighten": "git spawns per REPL/ACP boot come from presence.announce -> worktree_lease.currentIdentity (2 sequential rev-parse). Tighten to max 1 when the identity is memoized and uses one spawn, and to 0 once it is resolved in-process. Tighten security on the no-key path once there is a Keychain index.", + "_tighten": "REPL/ACP boot resolves the worktree identity in-process (git_dirs.zig) and spawns no git. Tighten security on the no-key path once there is a Keychain index.", "a_version": {"git": 0, "security": 0}, "a_help": {"git": 0, "security": 0}, - "b_repl_ready": {"git": {"max": 2, "target": 0}, "security": 0}, + "b_repl_ready": {"git": 0, "security": 0}, "c_first_request": {"git": 0, "security": 0}, - "d_acp_initialize": {"git": {"max": 2, "target": 0}, "security": 0}, + "d_acp_initialize": {"git": 0, "security": 0}, "d_acp_initialize_nokey": {"git": 0, "security": {"max": 26, "target": 0}} }, "git_repo": { - "_tighten": "Same as default. With GRAFF_AUTO_ISOLATE=off (always set by the benchmark) a one-commit repo also costs 2 git spawns per REPL/ACP boot.", + "_tighten": "Same as default: an ordinary one-commit repo is resolved in-process too (GRAFF_AUTO_ISOLATE=off is always set by the benchmark).", "a_version": {"git": 0, "security": 0}, "a_help": {"git": 0, "security": 0}, - "b_repl_ready": {"git": {"max": 2, "target": 1}, "security": 0}, + "b_repl_ready": {"git": 0, "security": 0}, "c_first_request": {"git": 0, "security": 0}, - "d_acp_initialize": {"git": {"max": 2, "target": 1}, "security": 0}, + "d_acp_initialize": {"git": 0, "security": 0}, "d_acp_initialize_nokey": {"git": 0, "security": {"max": 26, "target": 0}} } } From aabe845540c6fc88a41594306ee162d74fb6b157 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:10:55 +0800 Subject: [PATCH 08/21] scripts: split bench-startup helpers into bench_startup_lib.py Moves the process, fixture and report helpers into a sibling module so the benchmark script stays under the 600-line limit. No behaviour change: saved results load and compare byte-identically. Co-Authored-By: Codegraff --- scripts/bench-startup.py | 463 +-------------------------------- scripts/bench_startup_lib.py | 482 +++++++++++++++++++++++++++++++++++ 2 files changed, 487 insertions(+), 458 deletions(-) create mode 100644 scripts/bench_startup_lib.py diff --git a/scripts/bench-startup.py b/scripts/bench-startup.py index 01a0d8788..7786abade 100755 --- a/scripts/bench-startup.py +++ b/scripts/bench-startup.py @@ -18,25 +18,22 @@ import math import os import platform -import queue import re import shutil import signal -import socket import statistics import subprocess import sys import tempfile -import threading -import time from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent / "eval")) -from mock_model import ScriptedModel # noqa: E402 +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from bench_startup_lib import ( # noqa: E402 + LIVE, BenchError, Fixture, check_budgets, compare, cpu_name, parse_spawns, print_summary, + reap_group, run_stream, run_wall, start_mock, stats, tail, +) -clock = time.perf_counter_ns SCHEMA = 1 -PORT = 1234 # the built-in lmstudio provider URL is fixed to 127.0.0.1:1234 INIT = b'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}\n' BOOT_LINE = re.compile(r"^\[boot \+(\d+)ms, phase (\d+)ms\] (.+)$") # Safety vars are fixed; --env may not override them. @@ -44,326 +41,6 @@ "LMSTUDIO_API_KEY"} # Git subcommands that reach a remote or mean auto-isolation fired. Any of them fails the run. FORBIDDEN_GIT = ("fetch", "pull", "push", "clone", "ls-remote", "remote update", "worktree add") -LIVE = set() # process groups still to reap on exit - - -class BenchError(Exception): - pass - - -# ---------------------------------------------------------------- processes - -def spawn(argv, env, cwd, stdin=None, stdout=None, stderr=None): - """os.posix_spawn in its own process group. ~1 ms floor versus ~2.7 ms for - subprocess on macOS, so less interpreter overhead lands in the timer. - stdin/stdout/stderr: None for /dev/null, else an fd to dup2.""" - actions = [] - for target, fd in ((0, stdin), (1, stdout), (2, stderr)): - if fd is None: - flags = os.O_RDONLY if target == 0 else os.O_WRONLY - actions.append((os.POSIX_SPAWN_OPEN, target, os.devnull, flags, 0)) - else: - actions.append((os.POSIX_SPAWN_DUP2, fd, target)) - os.chdir(cwd) # posix_spawn has no chdir action before Python 3.13 - pid = os.posix_spawn(argv[0], argv, env, file_actions=actions, setpgroup=0) - LIVE.add(pid) - return pid - - -def reap_group(pid): - try: - os.killpg(pid, signal.SIGKILL) - except OSError: - pass - LIVE.discard(pid) - - -class Watchdog: - """Kills the process group at the deadline so a stuck run cannot hang.""" - def __init__(self, timeout): - self.fired = False - self.pid = None - self.timer = threading.Timer(timeout, self._fire) - self.timer.daemon = True - self.timer.start() - - def _fire(self): - self.fired = True - if self.pid is not None: - reap_group(self.pid) - - def cancel(self): - self.timer.cancel() - - -def wait_child(pid, dog): - _, status, usage = os.wait4(pid, 0) - ended = clock() - dog.cancel() - reap_group(pid) # stray grandchildren (git, companions) must not outlive the sample - rc = os.waitstatus_to_exitcode(status) if hasattr(os, "waitstatus_to_exitcode") else ( - -os.WTERMSIG(status) if os.WIFSIGNALED(status) else os.WEXITSTATUS(status)) - return ended, rc, (usage.ru_utime + usage.ru_stime) * 1e3 - - -def tail(path, limit=1200): - try: - return Path(path).read_text(errors="replace")[-limit:] - except OSError: - return "" - - -def run_wall(argv, env, cwd, timeout, errfile): - """Spawn → exit. stdout to /dev/null, stderr to errfile (for failure reports).""" - errfd = os.open(errfile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) - dog = Watchdog(timeout) - try: - started = clock() - pid = dog.pid = spawn(argv, env, cwd, stderr=errfd) - except OSError: - dog.cancel() - raise - finally: - os.close(errfd) - ended, rc, cpu = wait_child(pid, dog) - if dog.fired: - raise BenchError(f"{' '.join(argv[1:]) or argv[0]}: no exit within {timeout}s\n{tail(errfile)}") - return {"started": started, "wall_ms": (ended - started) / 1e6, "cpu_ms": cpu, "rc": rc} - - -def _reader(fd, tag, events): - buf = b"" - while True: - try: - chunk = os.read(fd, 65536) - except OSError: - chunk = b"" - stamp = clock() - if not chunk: - break - buf += chunk - while b"\n" in buf: - line, buf = buf.split(b"\n", 1) - events.put((stamp, tag, line.decode(errors="replace"))) - if buf: - events.put((clock(), tag, buf.decode(errors="replace"))) - events.put((clock(), tag, None)) - os.close(fd) - - -def run_stream(argv, env, cwd, timeout, ready, stdin_data=None): - """Spawn, optionally write stdin_data (stdin stays open), and stamp the first - line where ready(tag, line) holds. Then close stdin and time the exit.""" - events = queue.Queue() - out_r, out_w = os.pipe() - err_r, err_w = os.pipe() - in_r, in_w = os.pipe() if stdin_data is not None else (None, None) - readers = [threading.Thread(target=_reader, args=(fd, tag, events), daemon=True) - for fd, tag in ((out_r, "out"), (err_r, "err"))] - for thread in readers: - thread.start() - dog = Watchdog(timeout) - try: - started = clock() - pid = dog.pid = spawn(argv, env, cwd, stdin=in_r, stdout=out_w, stderr=err_w) - except OSError: - dog.cancel() # readers see EOF once the finally below closes the write ends - if in_w is not None: - os.close(in_w) - raise - finally: - for fd in (out_w, err_w, in_r): - if fd is not None: - os.close(fd) - deadline = time.monotonic() + timeout - lines, ready_at, eofs = [], None, 0 - try: - if stdin_data is not None: - os.write(in_w, stdin_data) - while ready_at is None and eofs < 2: - try: - stamp, tag, line = events.get(timeout=max(0.0, deadline - time.monotonic())) - except queue.Empty: - break - if line is None: - eofs += 1 - continue - lines.append((stamp, tag, line)) - if ready(tag, line): - ready_at = stamp - finally: - if in_w is not None: - try: - os.close(in_w) - except OSError: - pass - if ready_at is None: - reap_group(pid) # unblock wait4 now rather than at the watchdog deadline - ended, rc, cpu = wait_child(pid, dog) - for thread in readers: - thread.join(timeout=2) - while True: - try: - stamp, tag, line = events.get_nowait() - except queue.Empty: - break - if line is not None: - lines.append((stamp, tag, line)) - stderr_tail = "\n".join(l for _, t, l in lines if t == "err")[-1200:] - if ready_at is None: - why = "timed out" if time.monotonic() >= deadline or dog.fired else f"exited rc={rc}" - raise BenchError(f"{' '.join(argv[1:])}: ready marker never appeared ({why})\n{stderr_tail}") - return {"started": started, "ready_ms": (ready_at - started) / 1e6, - "wall_ms": (ended - started) / 1e6, "cpu_ms": cpu, "rc": rc, "lines": lines} - - -# ---------------------------------------------------------------- fixtures - -class Mock(ScriptedModel): - """scripts/eval/mock_model.py, plus a perf_counter_ns stamp per POST. The - stamp is taken once the request body has been read (loopback, sub-ms).""" - def __init__(self): - super().__init__([]) - self.posts = [] - - def next_reply(self, body): - stamp = clock() - with self._lock: - self.posts.append(stamp) - return super().next_reply(body) - - -def start_mock(): - probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - probe.settimeout(0.3) - try: - if probe.connect_ex(("127.0.0.1", PORT)) == 0: - raise BenchError(f"127.0.0.1:{PORT} is busy (LM Studio or another mock?). The lmstudio " - "provider is fixed to that port; stop the listener and rerun.") - finally: - probe.close() - mock = Mock() - try: - mock.start(PORT) - except OSError as exc: - raise BenchError(f"cannot bind 127.0.0.1:{PORT}: {exc}") from exc - return mock - - -def which_in(name, path): - found = shutil.which(name, path=path) - return os.path.realpath(found) if found else None - - -def write_exe(path, text): - path.write_text(text) - path.chmod(0o755) - - -class Fixture: - """Scratch tree: bin/ (timing PATH), shim/ (spawn-logging PATH), template - HOME and cwd per point. Each sample runs in fresh copies of the templates.""" - def __init__(self, args): - self.root = Path(tempfile.mkdtemp(prefix="graff-startup-")).resolve() - try: - self._setup(args) - except BaseException: - self.cleanup() - raise - - def _setup(self, args): - self.args = args - user_path = os.environ.get("PATH", "") - self.git = which_in("git", user_path) - if not self.git: - raise BenchError("git not found on PATH") - if sys.platform == "darwin" and self.git == "/usr/bin/git": - # /usr/bin/git is the xcrun shim: its first call under each new HOME - # costs ~70-110 ms (then ~11 ms). Every sample has a fresh HOME, so - # the shim would bill that to graff on every run. Use the real git. - found = subprocess.run(["xcrun", "--find", "git"], capture_output=True, text=True).stdout.strip() - if found and os.access(found, os.X_OK): - self.git = found - self.git_version = subprocess.run([self.git, "--version"], capture_output=True, - text=True).stdout.strip() - self.bin = self.root / "bin" - self.shim = self.root / "shim" - self.log = self.root / "spawns.log" - self.bin.mkdir() - self.shim.mkdir() - os.symlink(self.git, self.bin / "git") - # Never reach the real Keychain: answer "not found" (44) like an empty one. - stub = "#!/bin/sh\n{log}echo 'security: The specified item could not be found in the keychain.' >&2\nexit 44\n" - write_exe(self.bin / "security", stub.format(log="")) - write_exe(self.shim / "security", stub.format(log=f'printf "%s\\n" "security $*" >> "{self.log}"\n')) - write_exe(self.shim / "git", f'#!/bin/sh\nprintf "%s\\n" "git $*" >> "{self.log}"\nexec "{self.git}" "$@"\n') - self.companions = {} - if args.companion: - for name in ("codedb-pro", "muonry"): - real = which_in(name, user_path) - if real: - self.companions[name] = real - os.symlink(real, self.bin / name) - write_exe(self.shim / name, f'#!/bin/sh\nprintf "%s\\n" "{name} $*" >> "{self.log}"\nexec "{real}" "$@"\n') - if "codedb-pro" not in self.companions: - raise BenchError("--companion needs codedb-pro on PATH") - self.path = os.pathsep.join([str(self.bin), "/usr/bin", "/bin", "/usr/sbin", "/sbin"]) - self.shim_path = str(self.shim) + os.pathsep + self.path - self.hello = None - cc = which_in("cc", user_path) - if cc: - (self.root / "hello.c").write_text('#include \nint main(void){write(1,"hi\\n",3);return 0;}\n') - if subprocess.run([cc, "-O2", "-o", str(self.root / "hello"), str(self.root / "hello.c")], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: - self.hello = str(self.root / "hello") - self.counter = 0 - - def env(self, home, key=True, extra=None, path=None): - env = {k: os.environ[k] for k in ("TMPDIR", "TERM") if k in os.environ} - env.update(PATH=path or self.path, HOME=str(home), GRAFF_NO_TELEMETRY="1", - GRAFF_NO_UPDATE_CHECK="1", GRAFF_AUTO_ISOLATE="off", GRAFF_FLEET="off", - GRAFF_NO_SMOLIFY="1", GRAFF_LEARN_AUTO="off", NO_COLOR="1") - if key: - env["LMSTUDIO_API_KEY"] = "local" - env.update(self.args.extra_env) - env.update(extra or {}) - return env - - def git_env(self, home): - return dict(self.env(home, key=False), GIT_CONFIG_NOSYSTEM="1") - - def template(self, name): - base = self.root / "templates" / name - home, work = base / "home", base / "work" - home.mkdir(parents=True) - work.mkdir() - genv = self.git_env(home) - if self.args.git_repo: - (work / "README.md").write_text("startup benchmark fixture\n") - for cmd in (["init", "-q"], ["add", "README.md"], - ["-c", "user.name=bench", "-c", "user.email=bench@example.invalid", - "-c", "commit.gpgsign=false", "commit", "-q", "-m", "fixture"]): - subprocess.run([self.git] + cmd, cwd=work, env=genv, check=True, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - else: - inside = subprocess.run([self.git, "rev-parse", "--is-inside-work-tree"], cwd=work, env=genv, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 - if inside: - raise BenchError(f"scratch dir {work} is inside a git repository; set TMPDIR elsewhere") - return home, work - - def fresh(self, template): - """Copy a template HOME+cwd so every sample sees identical state.""" - self.counter += 1 - dest = self.root / "runs" / str(self.counter) - home = dest / "home" - work = dest / "work" - shutil.copytree(template[0], home, symlinks=True) - shutil.copytree(template[1], work, symlinks=True) - return dest, home, work - - def cleanup(self): - shutil.rmtree(self.root, ignore_errors=True) # ---------------------------------------------------------------- points @@ -438,27 +115,6 @@ def floor_sample(fx, prog, timeout): return run_wall([prog], fx.env(fx.root), str(work), timeout, str(fx.root / "floor.err"))["wall_ms"] -def stats(values): - ordered = sorted(values) - if not ordered: - return {"n": 0} - pick = lambda q: ordered[max(0, math.ceil(len(ordered) * q) - 1)] # noqa: E731 - return {"n": len(ordered), "min": round(ordered[0], 3), "median": round(statistics.median(ordered), 3), - "p95": round(pick(0.95), 3), "max": round(ordered[-1], 3)} - - -def parse_spawns(log, roots): - counts, detail = {}, [] - if log.exists(): - for line in log.read_text(errors="replace").splitlines(): - tool = line.split(" ", 1)[0] - counts[tool] = counts.get(tool, 0) + 1 - for root in roots: - line = line.replace(root, "") - detail.append(line) - return counts, detail - - def measure(args, binary): mock = start_mock() fx = None @@ -557,115 +213,6 @@ def log(msg): fx.cleanup() -# ---------------------------------------------------------------- report - -def cpu_name(): - try: - if sys.platform == "darwin": - return subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"], capture_output=True, - text=True, timeout=5).stdout.strip() - for line in Path("/proc/cpuinfo").read_text().splitlines(): - if line.startswith("model name"): - return line.split(":", 1)[1].strip() - except (OSError, subprocess.SubprocessError): - pass - return platform.processor() - - -def floor_ref(result): - floor = result.get("floor", {}) - for key in ("hello_ms", "true_ms"): - if floor.get(key, {}).get("n"): - return key, floor[key]["median"] - return None, None - - -def headline(result): - rows = [] - for name, point in result["points"].items(): - for metric, value in point.items(): - if isinstance(value, dict) and "median" in value and metric != "cpu_ms": - rows.append((f"{name}.{metric}", value)) - return rows - - -def print_summary(result): - key, floor = floor_ref(result) - print(f"floor {key}: median {floor} ms (subtract for graff-only cost)" if floor else "floor: none") - for k, v in result["floor"].items(): - print(f" {k:28} n={v['n']:3} min {v['min']:8.2f} med {v['median']:8.2f} p95 {v['p95']:8.2f}") - for label, v in headline(result): - print(f" {label:44} n={v['n']:3} min {v['min']:8.2f} med {v['median']:8.2f} p95 {v['p95']:8.2f} max {v['max']:8.2f}") - for name, point in result["points"].items(): - if "spawns" in point: - print(f" {name:44} spawns {point['spawns']}") - - -def variant(result): - env = result.get("env", {}) - return "git_repo" if env.get("git_repo") else "default" - - -def compare(before, after): - if variant(before) != variant(after) or before["env"].get("companion") != after["env"].get("companion"): - print("WARNING: comparing different variants (git_repo/companion differ)") - if before.get("host", {}).get("cpu") != after.get("host", {}).get("cpu"): - print("WARNING: different hosts") - _, fb = floor_ref(before) - _, fa = floor_ref(after) - print(f"{'point.metric':44} {'before':>9} {'after':>9} {'delta':>9} {'delta%':>7} net(before→after)") - old = dict(headline(before)) - for label, v in headline(after): - if label not in old: - continue - b, a = old[label]["median"], v["median"] - net = f"{b - (fb or 0):7.2f}→{a - (fa or 0):.2f}" - pct = (a - b) / b * 100 if b else 0.0 - print(f"{label:44} {b:9.2f} {a:9.2f} {a - b:+9.2f} {pct:+6.1f}% {net}") - for name, point in after["points"].items(): - prev = before["points"].get(name, {}).get("spawns") - if prev is not None and "spawns" in point and prev != point["spawns"]: - print(f"{name + ' spawns':44} {prev} → {point['spawns']}") - - -def check_budgets(result, budgets): - failures, notes = [], [] - key, floor = floor_ref(result) - if budgets.get("floor") and budgets["floor"] != key: - notes.append(f"budget floor {budgets['floor']} unavailable; using {key}") - for label, rule in budgets.get("ratio_over_floor", {}).items(): - if label.startswith("_"): - continue - name, metric = label.split(".", 1) - stat = result["points"].get(name, {}).get(metric) - if not stat or not floor: - notes.append(f"skip {label}: not measured") - continue - ratio = stat["median"] / floor - status = "ok" if ratio <= rule["max"] else "FAIL" - print(f"budget {label:40} ratio {ratio:6.1f} ≤ {rule['max']:<6} {status}") - if status != "ok": - failures.append(label) - for name, tools in budgets.get("spawns", {}).get(variant(result), {}).items(): - if name.startswith("_"): - continue - point = result["points"].get(name, {}) - if "spawns" not in point: - notes.append(f"skip {name} spawns: not measured") - continue - for tool, rule in tools.items(): - if tool.startswith("_"): - continue - limit = rule["max"] if isinstance(rule, dict) else rule - got = point["spawns"].get(tool, 0) - status = "ok" if got <= limit else "FAIL" - print(f"budget {name + ' ' + tool + ' spawns':40} {got:6} ≤ {limit:<6} {status}") - if status != "ok": - failures.append(f"{name}.{tool}") - for note in notes: - print(note) - return failures - def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) diff --git a/scripts/bench_startup_lib.py b/scripts/bench_startup_lib.py new file mode 100644 index 000000000..adb52bec8 --- /dev/null +++ b/scripts/bench_startup_lib.py @@ -0,0 +1,482 @@ +"""Process, fixture and report helpers for scripts/bench-startup.py. + +Split out of the benchmark script (hand-written files stay under 600 lines); +behaviour is unchanged. Nothing here is a benchmark point: the points, the +sampling loop and the CLI live in bench-startup.py. +""" +from __future__ import annotations + +import math +import os +import platform +import queue +import shutil +import signal +import socket +import statistics +import subprocess +import sys +import tempfile +import threading +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent / "eval")) +from mock_model import ScriptedModel # noqa: E402 + +clock = time.perf_counter_ns +PORT = 1234 # the built-in lmstudio provider URL is fixed to 127.0.0.1:1234 +LIVE = set() # process groups still to reap on exit + + + +class BenchError(Exception): + pass + + +# ---------------------------------------------------------------- processes + +def spawn(argv, env, cwd, stdin=None, stdout=None, stderr=None): + """os.posix_spawn in its own process group. ~1 ms floor versus ~2.7 ms for + subprocess on macOS, so less interpreter overhead lands in the timer. + stdin/stdout/stderr: None for /dev/null, else an fd to dup2.""" + actions = [] + for target, fd in ((0, stdin), (1, stdout), (2, stderr)): + if fd is None: + flags = os.O_RDONLY if target == 0 else os.O_WRONLY + actions.append((os.POSIX_SPAWN_OPEN, target, os.devnull, flags, 0)) + else: + actions.append((os.POSIX_SPAWN_DUP2, fd, target)) + os.chdir(cwd) # posix_spawn has no chdir action before Python 3.13 + pid = os.posix_spawn(argv[0], argv, env, file_actions=actions, setpgroup=0) + LIVE.add(pid) + return pid + + +def reap_group(pid): + try: + os.killpg(pid, signal.SIGKILL) + except OSError: + pass + LIVE.discard(pid) + + +class Watchdog: + """Kills the process group at the deadline so a stuck run cannot hang.""" + def __init__(self, timeout): + self.fired = False + self.pid = None + self.timer = threading.Timer(timeout, self._fire) + self.timer.daemon = True + self.timer.start() + + def _fire(self): + self.fired = True + if self.pid is not None: + reap_group(self.pid) + + def cancel(self): + self.timer.cancel() + + +def wait_child(pid, dog): + _, status, usage = os.wait4(pid, 0) + ended = clock() + dog.cancel() + reap_group(pid) # stray grandchildren (git, companions) must not outlive the sample + rc = os.waitstatus_to_exitcode(status) if hasattr(os, "waitstatus_to_exitcode") else ( + -os.WTERMSIG(status) if os.WIFSIGNALED(status) else os.WEXITSTATUS(status)) + return ended, rc, (usage.ru_utime + usage.ru_stime) * 1e3 + + +def tail(path, limit=1200): + try: + return Path(path).read_text(errors="replace")[-limit:] + except OSError: + return "" + + +def run_wall(argv, env, cwd, timeout, errfile): + """Spawn → exit. stdout to /dev/null, stderr to errfile (for failure reports).""" + errfd = os.open(errfile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + dog = Watchdog(timeout) + try: + started = clock() + pid = dog.pid = spawn(argv, env, cwd, stderr=errfd) + except OSError: + dog.cancel() + raise + finally: + os.close(errfd) + ended, rc, cpu = wait_child(pid, dog) + if dog.fired: + raise BenchError(f"{' '.join(argv[1:]) or argv[0]}: no exit within {timeout}s\n{tail(errfile)}") + return {"started": started, "wall_ms": (ended - started) / 1e6, "cpu_ms": cpu, "rc": rc} + + +def _reader(fd, tag, events): + buf = b"" + while True: + try: + chunk = os.read(fd, 65536) + except OSError: + chunk = b"" + stamp = clock() + if not chunk: + break + buf += chunk + while b"\n" in buf: + line, buf = buf.split(b"\n", 1) + events.put((stamp, tag, line.decode(errors="replace"))) + if buf: + events.put((clock(), tag, buf.decode(errors="replace"))) + events.put((clock(), tag, None)) + os.close(fd) + + +def run_stream(argv, env, cwd, timeout, ready, stdin_data=None): + """Spawn, optionally write stdin_data (stdin stays open), and stamp the first + line where ready(tag, line) holds. Then close stdin and time the exit.""" + events = queue.Queue() + out_r, out_w = os.pipe() + err_r, err_w = os.pipe() + in_r, in_w = os.pipe() if stdin_data is not None else (None, None) + readers = [threading.Thread(target=_reader, args=(fd, tag, events), daemon=True) + for fd, tag in ((out_r, "out"), (err_r, "err"))] + for thread in readers: + thread.start() + dog = Watchdog(timeout) + try: + started = clock() + pid = dog.pid = spawn(argv, env, cwd, stdin=in_r, stdout=out_w, stderr=err_w) + except OSError: + dog.cancel() # readers see EOF once the finally below closes the write ends + if in_w is not None: + os.close(in_w) + raise + finally: + for fd in (out_w, err_w, in_r): + if fd is not None: + os.close(fd) + deadline = time.monotonic() + timeout + lines, ready_at, eofs = [], None, 0 + try: + if stdin_data is not None: + os.write(in_w, stdin_data) + while ready_at is None and eofs < 2: + try: + stamp, tag, line = events.get(timeout=max(0.0, deadline - time.monotonic())) + except queue.Empty: + break + if line is None: + eofs += 1 + continue + lines.append((stamp, tag, line)) + if ready(tag, line): + ready_at = stamp + finally: + if in_w is not None: + try: + os.close(in_w) + except OSError: + pass + if ready_at is None: + reap_group(pid) # unblock wait4 now rather than at the watchdog deadline + ended, rc, cpu = wait_child(pid, dog) + for thread in readers: + thread.join(timeout=2) + while True: + try: + stamp, tag, line = events.get_nowait() + except queue.Empty: + break + if line is not None: + lines.append((stamp, tag, line)) + stderr_tail = "\n".join(l for _, t, l in lines if t == "err")[-1200:] + if ready_at is None: + why = "timed out" if time.monotonic() >= deadline or dog.fired else f"exited rc={rc}" + raise BenchError(f"{' '.join(argv[1:])}: ready marker never appeared ({why})\n{stderr_tail}") + return {"started": started, "ready_ms": (ready_at - started) / 1e6, + "wall_ms": (ended - started) / 1e6, "cpu_ms": cpu, "rc": rc, "lines": lines} + + +# ---------------------------------------------------------------- fixtures + +class Mock(ScriptedModel): + """scripts/eval/mock_model.py, plus a perf_counter_ns stamp per POST. The + stamp is taken once the request body has been read (loopback, sub-ms).""" + def __init__(self): + super().__init__([]) + self.posts = [] + + def next_reply(self, body): + stamp = clock() + with self._lock: + self.posts.append(stamp) + return super().next_reply(body) + + +def start_mock(): + probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + probe.settimeout(0.3) + try: + if probe.connect_ex(("127.0.0.1", PORT)) == 0: + raise BenchError(f"127.0.0.1:{PORT} is busy (LM Studio or another mock?). The lmstudio " + "provider is fixed to that port; stop the listener and rerun.") + finally: + probe.close() + mock = Mock() + try: + mock.start(PORT) + except OSError as exc: + raise BenchError(f"cannot bind 127.0.0.1:{PORT}: {exc}") from exc + return mock + + +def which_in(name, path): + found = shutil.which(name, path=path) + return os.path.realpath(found) if found else None + + +def write_exe(path, text): + path.write_text(text) + path.chmod(0o755) + + +class Fixture: + """Scratch tree: bin/ (timing PATH), shim/ (spawn-logging PATH), template + HOME and cwd per point. Each sample runs in fresh copies of the templates.""" + def __init__(self, args): + self.root = Path(tempfile.mkdtemp(prefix="graff-startup-")).resolve() + try: + self._setup(args) + except BaseException: + self.cleanup() + raise + + def _setup(self, args): + self.args = args + user_path = os.environ.get("PATH", "") + self.git = which_in("git", user_path) + if not self.git: + raise BenchError("git not found on PATH") + if sys.platform == "darwin" and self.git == "/usr/bin/git": + # /usr/bin/git is the xcrun shim: its first call under each new HOME + # costs ~70-110 ms (then ~11 ms). Every sample has a fresh HOME, so + # the shim would bill that to graff on every run. Use the real git. + found = subprocess.run(["xcrun", "--find", "git"], capture_output=True, text=True).stdout.strip() + if found and os.access(found, os.X_OK): + self.git = found + self.git_version = subprocess.run([self.git, "--version"], capture_output=True, + text=True).stdout.strip() + self.bin = self.root / "bin" + self.shim = self.root / "shim" + self.log = self.root / "spawns.log" + self.bin.mkdir() + self.shim.mkdir() + os.symlink(self.git, self.bin / "git") + # Never reach the real Keychain: answer "not found" (44) like an empty one. + stub = "#!/bin/sh\n{log}echo 'security: The specified item could not be found in the keychain.' >&2\nexit 44\n" + write_exe(self.bin / "security", stub.format(log="")) + write_exe(self.shim / "security", stub.format(log=f'printf "%s\\n" "security $*" >> "{self.log}"\n')) + write_exe(self.shim / "git", f'#!/bin/sh\nprintf "%s\\n" "git $*" >> "{self.log}"\nexec "{self.git}" "$@"\n') + self.companions = {} + if args.companion: + for name in ("codedb-pro", "muonry"): + real = which_in(name, user_path) + if real: + self.companions[name] = real + os.symlink(real, self.bin / name) + write_exe(self.shim / name, f'#!/bin/sh\nprintf "%s\\n" "{name} $*" >> "{self.log}"\nexec "{real}" "$@"\n') + if "codedb-pro" not in self.companions: + raise BenchError("--companion needs codedb-pro on PATH") + self.path = os.pathsep.join([str(self.bin), "/usr/bin", "/bin", "/usr/sbin", "/sbin"]) + self.shim_path = str(self.shim) + os.pathsep + self.path + self.hello = None + cc = which_in("cc", user_path) + if cc: + (self.root / "hello.c").write_text('#include \nint main(void){write(1,"hi\\n",3);return 0;}\n') + if subprocess.run([cc, "-O2", "-o", str(self.root / "hello"), str(self.root / "hello.c")], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: + self.hello = str(self.root / "hello") + self.counter = 0 + + def env(self, home, key=True, extra=None, path=None): + env = {k: os.environ[k] for k in ("TMPDIR", "TERM") if k in os.environ} + env.update(PATH=path or self.path, HOME=str(home), GRAFF_NO_TELEMETRY="1", + GRAFF_NO_UPDATE_CHECK="1", GRAFF_AUTO_ISOLATE="off", GRAFF_FLEET="off", + GRAFF_NO_SMOLIFY="1", GRAFF_LEARN_AUTO="off", NO_COLOR="1") + if key: + env["LMSTUDIO_API_KEY"] = "local" + env.update(self.args.extra_env) + env.update(extra or {}) + return env + + def git_env(self, home): + return dict(self.env(home, key=False), GIT_CONFIG_NOSYSTEM="1") + + def template(self, name): + base = self.root / "templates" / name + home, work = base / "home", base / "work" + home.mkdir(parents=True) + work.mkdir() + genv = self.git_env(home) + if self.args.git_repo: + (work / "README.md").write_text("startup benchmark fixture\n") + for cmd in (["init", "-q"], ["add", "README.md"], + ["-c", "user.name=bench", "-c", "user.email=bench@example.invalid", + "-c", "commit.gpgsign=false", "commit", "-q", "-m", "fixture"]): + subprocess.run([self.git] + cmd, cwd=work, env=genv, check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + else: + inside = subprocess.run([self.git, "rev-parse", "--is-inside-work-tree"], cwd=work, env=genv, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + if inside: + raise BenchError(f"scratch dir {work} is inside a git repository; set TMPDIR elsewhere") + return home, work + + def fresh(self, template): + """Copy a template HOME+cwd so every sample sees identical state.""" + self.counter += 1 + dest = self.root / "runs" / str(self.counter) + home = dest / "home" + work = dest / "work" + shutil.copytree(template[0], home, symlinks=True) + shutil.copytree(template[1], work, symlinks=True) + return dest, home, work + + def cleanup(self): + shutil.rmtree(self.root, ignore_errors=True) + + + + +def stats(values): + ordered = sorted(values) + if not ordered: + return {"n": 0} + pick = lambda q: ordered[max(0, math.ceil(len(ordered) * q) - 1)] # noqa: E731 + return {"n": len(ordered), "min": round(ordered[0], 3), "median": round(statistics.median(ordered), 3), + "p95": round(pick(0.95), 3), "max": round(ordered[-1], 3)} + + +def parse_spawns(log, roots): + counts, detail = {}, [] + if log.exists(): + for line in log.read_text(errors="replace").splitlines(): + tool = line.split(" ", 1)[0] + counts[tool] = counts.get(tool, 0) + 1 + for root in roots: + line = line.replace(root, "") + detail.append(line) + return counts, detail + + +# ---------------------------------------------------------------- report + +def cpu_name(): + try: + if sys.platform == "darwin": + return subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"], capture_output=True, + text=True, timeout=5).stdout.strip() + for line in Path("/proc/cpuinfo").read_text().splitlines(): + if line.startswith("model name"): + return line.split(":", 1)[1].strip() + except (OSError, subprocess.SubprocessError): + pass + return platform.processor() + + +def floor_ref(result): + floor = result.get("floor", {}) + for key in ("hello_ms", "true_ms"): + if floor.get(key, {}).get("n"): + return key, floor[key]["median"] + return None, None + + +def headline(result): + rows = [] + for name, point in result["points"].items(): + for metric, value in point.items(): + if isinstance(value, dict) and "median" in value and metric != "cpu_ms": + rows.append((f"{name}.{metric}", value)) + return rows + + +def print_summary(result): + key, floor = floor_ref(result) + print(f"floor {key}: median {floor} ms (subtract for graff-only cost)" if floor else "floor: none") + for k, v in result["floor"].items(): + print(f" {k:28} n={v['n']:3} min {v['min']:8.2f} med {v['median']:8.2f} p95 {v['p95']:8.2f}") + for label, v in headline(result): + print(f" {label:44} n={v['n']:3} min {v['min']:8.2f} med {v['median']:8.2f} p95 {v['p95']:8.2f} max {v['max']:8.2f}") + for name, point in result["points"].items(): + if "spawns" in point: + print(f" {name:44} spawns {point['spawns']}") + + +def variant(result): + env = result.get("env", {}) + return "git_repo" if env.get("git_repo") else "default" + + +def compare(before, after): + if variant(before) != variant(after) or before["env"].get("companion") != after["env"].get("companion"): + print("WARNING: comparing different variants (git_repo/companion differ)") + if before.get("host", {}).get("cpu") != after.get("host", {}).get("cpu"): + print("WARNING: different hosts") + _, fb = floor_ref(before) + _, fa = floor_ref(after) + print(f"{'point.metric':44} {'before':>9} {'after':>9} {'delta':>9} {'delta%':>7} net(before→after)") + old = dict(headline(before)) + for label, v in headline(after): + if label not in old: + continue + b, a = old[label]["median"], v["median"] + net = f"{b - (fb or 0):7.2f}→{a - (fa or 0):.2f}" + pct = (a - b) / b * 100 if b else 0.0 + print(f"{label:44} {b:9.2f} {a:9.2f} {a - b:+9.2f} {pct:+6.1f}% {net}") + for name, point in after["points"].items(): + prev = before["points"].get(name, {}).get("spawns") + if prev is not None and "spawns" in point and prev != point["spawns"]: + print(f"{name + ' spawns':44} {prev} → {point['spawns']}") + + +def check_budgets(result, budgets): + failures, notes = [], [] + key, floor = floor_ref(result) + if budgets.get("floor") and budgets["floor"] != key: + notes.append(f"budget floor {budgets['floor']} unavailable; using {key}") + for label, rule in budgets.get("ratio_over_floor", {}).items(): + if label.startswith("_"): + continue + name, metric = label.split(".", 1) + stat = result["points"].get(name, {}).get(metric) + if not stat or not floor: + notes.append(f"skip {label}: not measured") + continue + ratio = stat["median"] / floor + status = "ok" if ratio <= rule["max"] else "FAIL" + print(f"budget {label:40} ratio {ratio:6.1f} ≤ {rule['max']:<6} {status}") + if status != "ok": + failures.append(label) + for name, tools in budgets.get("spawns", {}).get(variant(result), {}).items(): + if name.startswith("_"): + continue + point = result["points"].get(name, {}) + if "spawns" not in point: + notes.append(f"skip {name} spawns: not measured") + continue + for tool, rule in tools.items(): + if tool.startswith("_"): + continue + limit = rule["max"] if isinstance(rule, dict) else rule + got = point["spawns"].get(tool, 0) + status = "ok" if got <= limit else "FAIL" + print(f"budget {name + ' ' + tool + ' spawns':40} {got:6} ≤ {limit:<6} {status}") + if status != "ok": + failures.append(f"{name}.{tool}") + for note in notes: + print(note) + return failures From ee366bd5be55d0b4f648c3e7fef77943a5620060 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:17:24 +0800 Subject: [PATCH 09/21] startup: prewarm the -p companion after credentials; one attempt; stop, not cancel - A prewarm attempt that ran to a result is the one attempt: take() returns its error instead of starting the companion again inline (a failing companion was spawned 4 times instead of 2). - main starts the prewarm after resolveKeys and setupWorktreeAndBanner and defers its discard, so a fatal credential or banner exit spawns nothing and an error return before the registry exists discards it. - discard no longer cancels the worker: a cancel delivered inside the bounded handshake's Io.Select left the select's read task running past its frame. The task-local registry publishes each spawned child (Registry.spawn_watch) and discard kills it, then joins. - BootPrewarm.tla models the fatal exits, the early return and stop-then-join, with ExitClean, NoExitStall and NoRepeatAfterResult and four new negative controls; ADR 0337 scopes the gain to a cold tool cache. Co-Authored-By: Codegraff --- ...e-shot-prewarms-the-companion-handshake.md | 71 +++-- formal/BootPrewarm.cfg | 8 +- formal/BootPrewarm.tla | 283 ++++++++++++------ formal/BootPrewarmEarlyStart.cfg | 10 + formal/BootPrewarmNoCwdKey.cfg | 4 + formal/BootPrewarmNoJoin.cfg | 4 + formal/BootPrewarmNoRetryAbort.cfg | 4 + formal/BootPrewarmNoStop.cfg | 10 + formal/BootPrewarmNoTeardownJoin.cfg | 4 + formal/BootPrewarmNoWindowDiscard.cfg | 10 + formal/BootPrewarmRetryFailed.cfg | 10 + formal/README.md | 5 +- formal/boot-prewarm.md | 103 +++++-- formal/change-evidence.md | 5 +- scripts/check-formal.py | 4 + src/boot_prewarm.zig | 250 +++++++++++++--- src/companion_boot.zig | 9 +- src/main.zig | 10 +- src/mcp.zig | 10 +- src/startup.zig | 1 - 20 files changed, 615 insertions(+), 200 deletions(-) create mode 100644 formal/BootPrewarmEarlyStart.cfg create mode 100644 formal/BootPrewarmNoStop.cfg create mode 100644 formal/BootPrewarmNoWindowDiscard.cfg create mode 100644 formal/BootPrewarmRetryFailed.cfg diff --git a/docs/adr/0337-a-one-shot-prewarms-the-companion-handshake.md b/docs/adr/0337-a-one-shot-prewarms-the-companion-handshake.md index 8e3e2317b..6da37c5a7 100644 --- a/docs/adr/0337-a-one-shot-prewarms-the-companion-handshake.md +++ b/docs/adr/0337-a-one-shot-prewarms-the-companion-handshake.md @@ -20,8 +20,10 @@ about 50 ms but took about 1.54 s to exit. ## Decision - **Overlap, not defer.** A one-shot starts the companion connect on its own - task (`boot_prewarm.startCompanion`) right after `runSubcommand`, and the - boot thread joins it in `connectCompanion`, exactly where `addServer` ran. + task (`boot_prewarm.startCompanion`) once credentials have resolved and + the banner has settled the cwd. `main` defers `boot_prewarm.discard` + right after the start. The boot thread joins the task in + `connectCompanion`, exactly where `addServer` ran. The handshake is not deferred past the first request, so the first request's tools, prompt and cache key are byte-identical to the serial path. @@ -34,31 +36,66 @@ about 50 ms but took about 1.54 s to exit. serial path. - **Join at the consumer, or discard.** `take` adopts the result only if the cwd, home and registry settings still match, awaits it, and merges it - under the registry mutex (ADR 0197). A mismatch, or a prewarm nobody - adopted, is cancelled, joined and torn down: in `connectCompanion`, and in - `Registry.deinitAtExit`. -- **A prewarm never loses the companion.** A quick failure falls back to the - inline `addServer`. A timeout is not repeated. + under the registry mutex (ADR 0197). Discard stops the attempt's child, + joins the task and tears the outcome down. It applies to a mismatch, or to + a prewarm nobody adopted, at three points: in `connectCompanion`, in + `Registry.deinitAtExit`, and on any return between the start and the + registry (`main`'s defer). The task is never cancelled: a cancel delivered + inside the bounded handshake's `Io.Select` returns before the select's + read task stops. Instead, the worker's task-local registry publishes each + child it spawns (`Registry.spawn_watch`, `boot_prewarm.Watch`). Discard + kills that child, so the handshake read ends at once. The worker forgets + its child before it reaps it, under the same lock, so a pid that may + already be reused is never signalled. +- **One attempt.** An attempt that ran to a result is the attempt the serial + path made. Its error is returned as `addServer`'s would have been, with the + same (empty) diagnostics; the companion is not started again. Only when no + attempt ran to a result (none started, or discarded on a mismatch) does the + consumer call `addServer` inline. - **Teardown ends retry pauses.** Each registry owns a heap event, `retry_abort`. `Registry.deinit` sets it before joining, and the pause in `mcp_start` waits on it. A start waiting to retry stops waiting and records its first failure. The registry still joins every task, so no thread outlives `io` (#325). -- **Formal evidence.** `formal/BootPrewarm.tla` holds the invariants - `JoinedBeforeUse`, `NoStaleAdoption`, `NoUseAfterTeardown` and - `NoRetryAfterAbort`, each with a negative control. +- **Formal evidence.** `formal/BootPrewarm.tla` models the fatal exits, the + early error return and both discards. Each of these invariants has a + negative control: `ExitClean` (no live companion and no unjoined worker + once the process exits), `NoExitStall`, `NoRepeatAfterResult`, + `JoinedBeforeUse`, `NoStaleAdoption` and `NoRetryAfterAbort`. The model + also checks `NoUseAfterTeardown`, but the runtime's join rules that out by + construction (`formal/boot-prewarm.md`). ## Consequences -- `-p` with an uncached companion reaches its first request about as fast as - `max(companion, rest of boot)` instead of their sum. A cached companion - (restored dormant) gains nothing and costs nothing. +- The gain applies to a cold MCP tool cache only: the first `-p` per HOME, + or the first after the companion binary changes. There, `-p` reaches its + first request in about `max(companion, rest of boot)` instead of their + sum. With a warm cache the companion is restored dormant in both paths: + the prewarm task spawns no process, and gains and costs nothing + measurable. The pre-series binary spawned none either. +- Measured on macOS, hermetic `-p hi --model lmstudio`, fresh HOME with 15 + plugins, a stub companion that answers after 6 ms. Medians of the first + POST, 20 runs each, alternating with the pre-series binary: + + | Companion | Pre-series | Now | Companion spawns per run | + | --- | --- | --- | --- | + | answers, cold tool cache | 22.1 ms | 18.6 ms | 1 / 1 | + | answers, warm tool cache | 12.6 ms | 12.4 ms | 0 / 0 | + | fails (exits 1) | 24.5 ms | 20.2 ms | 2 / 2 | + | not on PATH | 11.6 ms | 11.7 ms | 0 / 0 | + + A failing companion is spawned twice by both, because the stdio probe + respawns a process that closed. The first revision of this change retried + the failed attempt inline: 4 spawns and 32.2 ms. - The companion's connect line on stderr can print earlier than other boot diagnostics. -- When boot fails after the prewarm started (no credential, an error before - the registry exists), the companion has already been spawned. Process - exit closes its pipe, and `std.process.Init` joins the worker, which is - bounded by the handshake timeout. +- A run that fails on credentials or in the banner spawns no companion, as + before. Those paths exit through `std.process.fatal`, which runs no defer. + Credential resolution measures about 0 ms for a one-shot with an + environment key, so starting after it costs no overlap. With a + Keychain-only credential the lookups no longer overlap the handshake. +- On Windows, `Watch` has no signal, so a discard waits for the attempt, + which the handshake timeout bounds. - An interrupted retry is not attempted again in that process; the next session starts the server again. - The plugin scan is not prewarmed. `plugins.applyEnv` stores diff --git a/formal/BootPrewarm.cfg b/formal/BootPrewarm.cfg index ecbf19d23..a18c9177a 100644 --- a/formal/BootPrewarm.cfg +++ b/formal/BootPrewarm.cfg @@ -3,12 +3,18 @@ CONSTANTS BypassJoin = FALSE BypassCwdKey = FALSE BypassTeardownJoin = FALSE BypassRetryAbort = FALSE + StartBeforeKeys = FALSE + BypassWindowDiscard = FALSE + BypassStop = FALSE + RetryFailed = FALSE INVARIANTS TypeOK JoinedBeforeUse NoStaleAdoption NoUseAfterTeardown - NoLeakAtExit + ExitClean + NoExitStall AtMostOneCompanion + NoRepeatAfterResult NeverMissing NoRetryAfterAbort PROPERTIES TeardownFinishes diff --git a/formal/BootPrewarm.tla b/formal/BootPrewarm.tla index afc78015c..59b4e15ef 100644 --- a/formal/BootPrewarm.tla +++ b/formal/BootPrewarm.tla @@ -1,75 +1,157 @@ ------------------------------ MODULE BootPrewarm ------------------------------ EXTENDS Naturals, TLC -\* A finite abstraction of the boot-thread companion prewarm (boot_prewarm.zig) +\* A finite abstraction of the one-shot companion prewarm (boot_prewarm.zig) \* and of one registry start task's retry pause at teardown (mcp_start.zig). \* The prewarm worker connects the companion before the registry exists; the -\* boot thread joins it at its one consumer (connectCompanion) or discards it. -CONSTANTS BypassJoin, BypassCwdKey, BypassTeardownJoin, BypassRetryAbort +\* boot thread joins it at its one consumer (connectCompanion) or discards it +\* (stop the worker's child, then join). Boot can also leave early: a fatal +\* exit while resolving credentials or in the banner (no defer runs), or an +\* error return between the prewarm start and the registry (main's deferred +\* discard runs, then std.process.Init joins the remaining workers). +CONSTANTS BypassJoin, BypassCwdKey, BypassTeardownJoin, BypassRetryAbort, + StartBeforeKeys, BypassWindowDiscard, BypassStop, RetryFailed ASSUME /\ BypassJoin \in BOOLEAN /\ BypassCwdKey \in BOOLEAN /\ BypassTeardownJoin \in BOOLEAN /\ BypassRetryAbort \in BOOLEAN + /\ StartBeforeKeys \in BOOLEAN + /\ BypassWindowDiscard \in BOOLEAN + /\ BypassStop \in BOOLEAN + /\ RetryFailed \in BOOLEAN -BootStates == {"pre", "consumer", "inline", "connected", "noConsumer", "joining", "exited"} +BootStates == {"keys", "banner", "start", "window", "pre", "consumer", "mismatch", + "inline", "connected", "noConsumer", "discarding", "joining", "exited"} WorkerStates == {"none", "running", "done", "consumed"} -TaskStates == {"attempt", "pause", "retry", "done"} +\* The companion process the prewarm spawned: owned by the worker or its +\* outcome ("alive"), adopted by the registry ("registry"), or reaped. +ChildStates == {"none", "alive", "registry", "reaped"} +TaskStates == {"idle", "attempt", "pause", "retry", "done"} -VARIABLES boot, worker, result, spawned, adopted, inlined, registered, - cwdMoved, cwdAtTake, usedEarly, ioAlive, task, abort, retriedAfterAbort +VARIABLES boot, worker, result, child, hangs, stopReq, stalled, adopted, + inlined, taken, registered, cwdMoved, cwdAtTake, usedEarly, ioAlive, + regUp, task, abort, retriedAfterAbort -vars == <> +vars == <> -prewarm == <> +prewarm == <> taskVars == <> +others == <> -\* The gate either declined (no slot) or started the worker after -\* runSubcommand; the registry's own start task is in its first attempt. +\* `hangs`: a companion that never answers its handshake (only the 15 s +\* handshake timeout, or the discard's stop, ends the attempt). With +\* StartBeforeKeys the gate ran before credentials, as it once did. Init == - /\ boot = "pre" - /\ worker \in {"none", "running"} + /\ boot = "keys" + /\ worker \in IF StartBeforeKeys THEN {"none", "running"} ELSE {"none"} + /\ child = IF worker = "running" THEN "alive" ELSE "none" + /\ hangs \in BOOLEAN /\ result = "none" - /\ spawned = (worker = "running") + /\ stopReq = FALSE + /\ stalled = FALSE /\ adopted = FALSE /\ inlined = FALSE + /\ taken = FALSE /\ registered = 0 /\ cwdMoved = FALSE /\ cwdAtTake = FALSE /\ usedEarly = FALSE /\ ioAlive = TRUE - /\ task = "attempt" + /\ regUp = FALSE + /\ task = "idle" /\ abort = FALSE /\ retriedAfterAbort = FALSE -\* connectTask: one attempt on a task-local registry. A failure leaves no -\* process; success leaves a prewarm-owned server. -WorkerFinish == - /\ worker = "running" /\ ioAlive - /\ \E r \in {"ok", "fail"}: - /\ result' = r - /\ spawned' = (r = "ok" /\ ~adopted) - /\ worker' = "done" - /\ UNCHANGED <> /\ UNCHANGED taskVars - -\* -w or auto-isolation between prewarm start and the consumer. +\* ---------------------------------------------------------------- boot + +KeysOk == + /\ boot = "keys" /\ boot' = "banner" + /\ UNCHANGED prewarm /\ UNCHANGED others /\ UNCHANGED taskVars + +\* resolveKeys or setupWorktreeAndBanner exits through std.process.fatal: +\* no defer runs, no worker is joined, a spawned child is left behind. +FatalExit == + /\ boot \in {"keys", "banner"} /\ boot' = "exited" + /\ UNCHANGED prewarm /\ UNCHANGED others /\ UNCHANGED taskVars + +BannerOk == + /\ boot = "banner" /\ boot' = "start" + /\ UNCHANGED prewarm /\ UNCHANGED others /\ UNCHANGED taskVars + +\* main: startCompanion (the gate declines or starts the worker, which +\* spawns the child), then `defer discard`. +Start == + /\ boot = "start" /\ boot' = "window" + /\ IF StartBeforeKeys + THEN UNCHANGED <> + ELSE \E w \in {"none", "running"}: + /\ worker' = w + /\ child' = IF w = "running" THEN "alive" ELSE "none" + /\ UNCHANGED <> + /\ UNCHANGED others /\ UNCHANGED taskVars + +\* A failed `try` before initRegistryConsent returns: the deferred discard +\* stops and joins the worker. +WindowError == + /\ boot = "window" + /\ IF BypassWindowDiscard + THEN boot' = "joining" /\ UNCHANGED stopReq + ELSE boot' = "discarding" /\ stopReq' = ~BypassStop + /\ UNCHANGED <> + /\ UNCHANGED others /\ UNCHANGED taskVars + +\* initRegistryConsent returned; `defer registry_storage.deinitAtExit()`. +RegistryUp == + /\ boot = "window" /\ boot' = "pre" + /\ regUp' = TRUE /\ task' = "attempt" + /\ UNCHANGED prewarm + /\ UNCHANGED <> + +\* Any chdir between the prewarm start and the consumer. MoveCwd == - /\ boot = "pre" /\ ~cwdMoved + /\ boot \in {"window", "pre"} /\ ~cwdMoved /\ cwdMoved' = TRUE - /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + /\ UNCHANGED <> + /\ UNCHANGED prewarm /\ UNCHANGED taskVars ReachConsumer == - /\ boot = "pre" - /\ boot' = "consumer" - /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + /\ boot = "pre" /\ boot' = "consumer" + /\ UNCHANGED prewarm /\ UNCHANGED others /\ UNCHANGED taskVars \* A path that exits after the registry exists without reaching the \* consumer (--selftest-spinner); deinitAtExit discards the slot. SkipConsumer == - /\ boot = "pre" - /\ boot' = "noConsumer" - /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + /\ boot = "pre" /\ boot' = "noConsumer" + /\ UNCHANGED prewarm /\ UNCHANGED others /\ UNCHANGED taskVars + +\* ---------------------------------------------------------------- worker + +\* connectTask: one startServer on a task-local registry. A failure reaps +\* its child; success hands the child to the outcome. A stopped child ends +\* the handshake at once. +WorkerFinish == + /\ worker = "running" /\ ioAlive /\ boot # "exited" + /\ ~hangs \/ stopReq + /\ \E r \in {"ok", "fail"}: + /\ result' = r + /\ child' = IF r = "ok" THEN "alive" ELSE "reaped" + /\ worker' = "done" + /\ UNCHANGED <> + /\ UNCHANGED others /\ UNCHANGED taskVars + +\* A silent companion's attempt ends only at the handshake timeout; an exit +\* path that waits for it has stalled. +WorkerTimeout == + /\ worker = "running" /\ ioAlive /\ boot # "exited" + /\ hangs /\ ~stopReq + /\ result' = "fail" /\ child' = "reaped" /\ worker' = "done" + /\ stalled' = (boot \in {"discarding", "joining"}) + /\ UNCHANGED <> + /\ UNCHANGED others /\ UNCHANGED taskVars + +\* ---------------------------------------------------------------- consumer KeyMatches == ~cwdMoved \/ BypassCwdKey @@ -81,64 +163,82 @@ TakeAdopt == /\ usedEarly' = (worker = "running") /\ cwdAtTake' = cwdMoved /\ worker' = IF worker = "done" THEN "consumed" ELSE worker - /\ adopted' = TRUE - /\ spawned' = FALSE + /\ adopted' = TRUE /\ taken' = TRUE + /\ child' = "registry" /\ registered' = registered + 1 /\ boot' = "connected" - /\ UNCHANGED <> /\ UNCHANGED taskVars + /\ UNCHANGED <> + /\ UNCHANGED taskVars -\* A failed prewarm falls back to addServer on the boot thread. +\* A failed attempt is the one attempt: its error is what addServer would +\* have returned. RetryFailed is the earlier fallback that started it again. TakeFailed == /\ boot = "consumer" /\ KeyMatches /\ worker = "done" /\ result = "fail" - /\ worker' = "consumed" - /\ boot' = "inline" - /\ UNCHANGED <> /\ UNCHANGED taskVars + /\ worker' = "consumed" /\ taken' = TRUE + /\ boot' = IF RetryFailed THEN "inline" ELSE "connected" + /\ UNCHANGED <> + /\ UNCHANGED others /\ UNCHANGED taskVars -\* A key mismatch discards: Future.cancel is a joined operation, then the -\* server is torn down. +\* A key mismatch discards (stop the child, join, tear the outcome down), +\* then connects inline: no attempt's result was used. TakeMismatch == - /\ boot = "consumer" /\ ~KeyMatches - /\ worker \in {"running", "done"} - /\ worker' = "consumed" - /\ spawned' = FALSE + /\ boot = "consumer" /\ ~KeyMatches /\ worker # "none" + /\ stopReq' = ~BypassStop /\ boot' = "mismatch" + /\ UNCHANGED <> + /\ UNCHANGED others /\ UNCHANGED taskVars + +MismatchJoin == + /\ boot = "mismatch" /\ worker # "running" + /\ worker' = IF worker = "done" THEN "consumed" ELSE worker + /\ child' = IF child = "alive" THEN "reaped" ELSE child /\ boot' = "inline" - /\ UNCHANGED <> /\ UNCHANGED taskVars + /\ UNCHANGED <> + /\ UNCHANGED others /\ UNCHANGED taskVars TakeNone == /\ boot = "consumer" /\ worker = "none" /\ boot' = "inline" - /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + /\ UNCHANGED prewarm /\ UNCHANGED others /\ UNCHANGED taskVars InlineConnect == /\ boot = "inline" /\ \E ok \in BOOLEAN: registered' = registered + (IF ok THEN 1 ELSE 0) /\ inlined' = TRUE /\ boot' = "connected" - /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + /\ UNCHANGED prewarm /\ UNCHANGED <> /\ UNCHANGED taskVars -\* deinitAtExit: discard any unadopted prewarm (cancel + join), then +\* ---------------------------------------------------------------- exit + +\* deinitAtExit: discard (stop the child, join the worker), then \* Registry.deinit sets retry_abort and joins its start tasks. Teardown == /\ boot \in {"connected", "noConsumer"} - /\ IF BypassTeardownJoin - THEN UNCHANGED <> - ELSE /\ worker' = IF worker \in {"running", "done"} THEN "consumed" ELSE worker - /\ spawned' = FALSE - /\ abort' = TRUE + /\ boot' = "discarding" /\ stopReq' = ~BypassStop + /\ UNCHANGED <> + /\ UNCHANGED others /\ UNCHANGED taskVars + +\* discard's join and teardown, then (registry up) Registry.deinit. +DiscardJoin == + /\ boot = "discarding" + /\ worker # "running" \/ BypassTeardownJoin + /\ worker' = IF worker = "done" THEN "consumed" ELSE worker + /\ child' = IF child = "alive" \/ (child = "registry" /\ regUp) THEN "reaped" ELSE child + /\ abort' = (abort \/ regUp) /\ boot' = "joining" - /\ UNCHANGED <> /\ UNCHANGED taskVars + /\ UNCHANGED <> + /\ UNCHANGED <> /\ UNCHANGED taskVars -\* main returns; std.process.Init tears the Io down after its join. +\* main returns; std.process.Init joins its workers, then tears the Io down. IoDown == /\ boot = "joining" - /\ BypassTeardownJoin \/ task = "done" + /\ BypassTeardownJoin \/ (worker # "running" /\ task \in {"idle", "done"}) /\ ioAlive' = FALSE /\ boot' = "exited" - /\ UNCHANGED <> /\ UNCHANGED prewarm /\ UNCHANGED taskVars + /\ UNCHANGED prewarm + /\ UNCHANGED <> /\ UNCHANGED taskVars + +\* ---------------------------------------------------------------- start task \* The registry's start task: one attempt, a transient failure pauses, and \* the pause either times out into the one retry or ends on retry_abort. @@ -146,46 +246,50 @@ AttemptEnds == /\ task = "attempt" /\ ioAlive /\ task' \in {"pause", "done"} /\ UNCHANGED retriedAfterAbort - /\ UNCHANGED <> /\ UNCHANGED prewarm + /\ UNCHANGED boot /\ UNCHANGED others /\ UNCHANGED prewarm PauseTimesOut == /\ task = "pause" /\ ioAlive /\ ~abort \/ BypassRetryAbort /\ task' = "retry" /\ retriedAfterAbort' = abort - /\ UNCHANGED <> /\ UNCHANGED prewarm + /\ UNCHANGED boot /\ UNCHANGED others /\ UNCHANGED prewarm PauseAborted == /\ task = "pause" /\ abort /\ ~BypassRetryAbort /\ task' = "done" /\ UNCHANGED retriedAfterAbort - /\ UNCHANGED <> /\ UNCHANGED prewarm + /\ UNCHANGED boot /\ UNCHANGED others /\ UNCHANGED prewarm RetryEnds == /\ task = "retry" /\ ioAlive /\ task' = "done" /\ UNCHANGED retriedAfterAbort - /\ UNCHANGED <> /\ UNCHANGED prewarm + /\ UNCHANGED boot /\ UNCHANGED others /\ UNCHANGED prewarm Next == - \/ WorkerFinish \/ MoveCwd \/ ReachConsumer \/ SkipConsumer - \/ TakeAdopt \/ TakeFailed \/ TakeMismatch \/ TakeNone \/ InlineConnect - \/ Teardown \/ IoDown + \/ KeysOk \/ FatalExit \/ BannerOk \/ Start \/ WindowError \/ RegistryUp + \/ MoveCwd \/ ReachConsumer \/ SkipConsumer + \/ WorkerFinish \/ WorkerTimeout + \/ TakeAdopt \/ TakeFailed \/ TakeMismatch \/ MismatchJoin \/ TakeNone \/ InlineConnect + \/ Teardown \/ DiscardJoin \/ IoDown \/ AttemptEnds \/ PauseTimesOut \/ PauseAborted \/ RetryEnds Spec == Init /\ [][Next]_vars - /\ WF_vars(WorkerFinish) /\ WF_vars(ReachConsumer) - /\ WF_vars(TakeAdopt \/ TakeFailed \/ TakeMismatch \/ TakeNone \/ InlineConnect) - /\ WF_vars(Teardown) /\ WF_vars(IoDown) + /\ WF_vars(WorkerFinish) /\ WF_vars(WorkerTimeout) + /\ WF_vars(TakeAdopt \/ TakeFailed \/ TakeMismatch \/ MismatchJoin \/ TakeNone \/ InlineConnect) + /\ WF_vars(Teardown) /\ WF_vars(DiscardJoin) /\ WF_vars(IoDown) /\ WF_vars(AttemptEnds \/ PauseTimesOut \/ PauseAborted \/ RetryEnds) TypeOK == /\ boot \in BootStates /\ worker \in WorkerStates + /\ child \in ChildStates /\ result \in {"none", "ok", "fail"} /\ registered \in 0..2 /\ task \in TaskStates - /\ {spawned, adopted, inlined, cwdMoved, cwdAtTake, usedEarly, ioAlive, abort, retriedAfterAbort} \subseteq BOOLEAN + /\ {hangs, stopReq, stalled, adopted, inlined, taken, cwdMoved, cwdAtTake, + usedEarly, ioAlive, regUp, abort, retriedAfterAbort} \subseteq BOOLEAN \* The boot thread never uses the prewarm outcome before the worker ended. JoinedBeforeUse == ~usedEarly @@ -193,21 +297,32 @@ JoinedBeforeUse == ~usedEarly \* A prewarm recorded in another directory is never adopted. NoStaleAdoption == adopted => ~cwdAtTake -\* No thread still uses the Io after it is torn down. -NoUseAfterTeardown == ~ioAlive => (worker # "running" /\ task = "done") +\* No thread still uses the Io after it is torn down. std.process.Init joins +\* every worker before it deinitializes the Io, so the runtime rules this +\* out by construction; only BypassTeardownJoin (no such join) violates it. +NoUseAfterTeardown == ~ioAlive => (worker # "running" /\ task \in {"idle", "done"}) -\* No prewarm-owned server outlives the registry teardown. -NoLeakAtExit == boot = "exited" => ~spawned +\* The hazard of every exit path: once the process has exited, no companion +\* the prewarm spawned is alive (prewarm-owned or registered) and no +\* prewarm worker was left unjoined. +ExitClean == boot = "exited" => (child \notin {"alive", "registry"} /\ worker # "running") + +\* No exit path waits out a silent companion's handshake timeout. +NoExitStall == ~stalled \* The companion is registered once at most: adopted or inline, never both. AtMostOneCompanion == registered <= 1 /\ ~(adopted /\ inlined) -\* Reaching the consumer always ends adopted or with the inline connect tried. -NeverMissing == boot = "connected" => (adopted \/ inlined) +\* An attempt that ran to a result is the one attempt. +NoRepeatAfterResult == ~(taken /\ inlined) + +\* Reaching the consumer always ends adopted, with an attempt's result, or +\* with the inline connect tried. +NeverMissing == boot = "connected" => (adopted \/ taken \/ inlined) \* Once teardown signals, no retry attempt starts. NoRetryAfterAbort == ~retriedAfterAbort -\* Under the fairness above, a started teardown finishes. -TeardownFinishes == (boot = "joining") ~> (boot = "exited") +\* Under the fairness above, every exit that started finishes. +TeardownFinishes == (boot \in {"discarding", "joining"}) ~> (boot = "exited") ============================================================================= diff --git a/formal/BootPrewarmEarlyStart.cfg b/formal/BootPrewarmEarlyStart.cfg new file mode 100644 index 000000000..878f76a23 --- /dev/null +++ b/formal/BootPrewarmEarlyStart.cfg @@ -0,0 +1,10 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = FALSE + BypassCwdKey = FALSE + BypassTeardownJoin = FALSE + BypassRetryAbort = FALSE + StartBeforeKeys = TRUE + BypassWindowDiscard = FALSE + BypassStop = FALSE + RetryFailed = FALSE +INVARIANT ExitClean diff --git a/formal/BootPrewarmNoCwdKey.cfg b/formal/BootPrewarmNoCwdKey.cfg index acc23bcc7..59673c8da 100644 --- a/formal/BootPrewarmNoCwdKey.cfg +++ b/formal/BootPrewarmNoCwdKey.cfg @@ -3,4 +3,8 @@ CONSTANTS BypassJoin = FALSE BypassCwdKey = TRUE BypassTeardownJoin = FALSE BypassRetryAbort = FALSE + StartBeforeKeys = FALSE + BypassWindowDiscard = FALSE + BypassStop = FALSE + RetryFailed = FALSE INVARIANT NoStaleAdoption diff --git a/formal/BootPrewarmNoJoin.cfg b/formal/BootPrewarmNoJoin.cfg index cc61365a4..09aa186e8 100644 --- a/formal/BootPrewarmNoJoin.cfg +++ b/formal/BootPrewarmNoJoin.cfg @@ -3,4 +3,8 @@ CONSTANTS BypassJoin = TRUE BypassCwdKey = FALSE BypassTeardownJoin = FALSE BypassRetryAbort = FALSE + StartBeforeKeys = FALSE + BypassWindowDiscard = FALSE + BypassStop = FALSE + RetryFailed = FALSE INVARIANT JoinedBeforeUse diff --git a/formal/BootPrewarmNoRetryAbort.cfg b/formal/BootPrewarmNoRetryAbort.cfg index 8e8750244..ae8065215 100644 --- a/formal/BootPrewarmNoRetryAbort.cfg +++ b/formal/BootPrewarmNoRetryAbort.cfg @@ -3,4 +3,8 @@ CONSTANTS BypassJoin = FALSE BypassCwdKey = FALSE BypassTeardownJoin = FALSE BypassRetryAbort = TRUE + StartBeforeKeys = FALSE + BypassWindowDiscard = FALSE + BypassStop = FALSE + RetryFailed = FALSE INVARIANT NoRetryAfterAbort diff --git a/formal/BootPrewarmNoStop.cfg b/formal/BootPrewarmNoStop.cfg new file mode 100644 index 000000000..68d4d0ae6 --- /dev/null +++ b/formal/BootPrewarmNoStop.cfg @@ -0,0 +1,10 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = FALSE + BypassCwdKey = FALSE + BypassTeardownJoin = FALSE + BypassRetryAbort = FALSE + StartBeforeKeys = FALSE + BypassWindowDiscard = FALSE + BypassStop = TRUE + RetryFailed = FALSE +INVARIANT NoExitStall diff --git a/formal/BootPrewarmNoTeardownJoin.cfg b/formal/BootPrewarmNoTeardownJoin.cfg index 9cd198d36..0549900c1 100644 --- a/formal/BootPrewarmNoTeardownJoin.cfg +++ b/formal/BootPrewarmNoTeardownJoin.cfg @@ -3,4 +3,8 @@ CONSTANTS BypassJoin = FALSE BypassCwdKey = FALSE BypassTeardownJoin = TRUE BypassRetryAbort = FALSE + StartBeforeKeys = FALSE + BypassWindowDiscard = FALSE + BypassStop = FALSE + RetryFailed = FALSE INVARIANT NoUseAfterTeardown diff --git a/formal/BootPrewarmNoWindowDiscard.cfg b/formal/BootPrewarmNoWindowDiscard.cfg new file mode 100644 index 000000000..f17d432ab --- /dev/null +++ b/formal/BootPrewarmNoWindowDiscard.cfg @@ -0,0 +1,10 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = FALSE + BypassCwdKey = FALSE + BypassTeardownJoin = FALSE + BypassRetryAbort = FALSE + StartBeforeKeys = FALSE + BypassWindowDiscard = TRUE + BypassStop = FALSE + RetryFailed = FALSE +INVARIANT ExitClean diff --git a/formal/BootPrewarmRetryFailed.cfg b/formal/BootPrewarmRetryFailed.cfg new file mode 100644 index 000000000..f71e33177 --- /dev/null +++ b/formal/BootPrewarmRetryFailed.cfg @@ -0,0 +1,10 @@ +SPECIFICATION Spec +CONSTANTS BypassJoin = FALSE + BypassCwdKey = FALSE + BypassTeardownJoin = FALSE + BypassRetryAbort = FALSE + StartBeforeKeys = FALSE + BypassWindowDiscard = FALSE + BypassStop = FALSE + RetryFailed = TRUE +INVARIANT NoRepeatAfterResult diff --git a/formal/README.md b/formal/README.md index e82d4eff3..4db2f35f6 100644 --- a/formal/README.md +++ b/formal/README.md @@ -13,8 +13,9 @@ are difficult to reproduce with ordinary tests. - [HTTP/2 leases](http2-leases.md): exclusive ownership, idle pooling, cancellation, shutdown, and reuse only after stream completion. - [Boot prewarm](boot-prewarm.md): a one-shot's companion connect joined at - its one consumer, discarded on a cwd or setting mismatch, and teardown - ending a start's retry pause while still joining it. + its one consumer, discarded on a cwd or setting mismatch or on any early + exit with no child or worker left behind, and teardown ending a start's + retry pause while still joining it. Each model has a normal configuration and a deliberately broken configuration. The negative control must fail the intended invariant. A syntax error, timeout, diff --git a/formal/boot-prewarm.md b/formal/boot-prewarm.md index 7f02b036a..ec7cf23ab 100644 --- a/formal/boot-prewarm.md +++ b/formal/boot-prewarm.md @@ -1,35 +1,53 @@ # Boot prewarm and retry abort `BootPrewarm.tla` is a finite model of two lifecycles that decide whether a -worker can still use the process `Io` when boot or teardown moves on: +worker, or a process it spawned, can outlive the decision to exit: - the companion prewarm (`src/boot_prewarm.zig`, ADR 0337): a worker connects `codedb-pro --mcp` before the registry exists, and the boot - thread joins it at its one consumer, `connectCompanion`; + thread joins it at its one consumer, `connectCompanion`, or discards it; - one registry start task's retry pause (`src/mcp_start.zig`): teardown must not wait out the pause, and must still join the task (#325). | Model action | Implementation boundary | | --- | --- | -| `Init` (worker `running` or `none`) | `startup.resolveKeysOrRunAcpPreAuth` calls `boot_prewarm.startCompanion` after `runSubcommand`; the gate (`wantsCompanion`, no project `.mcp.json`, opt-out, PATH) either declines or `start` launches `connectTask` with `io.concurrent`. | -| `WorkerFinish` | `connectTask`: one `startServer` on a task-local registry; a failure tears its server down before returning. | -| `MoveCwd` | `-w` or auto-isolation changing the cwd between start and consumer. | -| `TakeAdopt`, `TakeFailed`, `TakeMismatch`, `TakeNone` | `boot_prewarm.take`: `matches` (cwd key, home, probe and lazy settings), `Future.await`, then `mcp_boot.adoptStarted` under the registry mutex; a fast failure returns null so the caller runs `addServer`; a mismatch calls `discard`. | -| `InlineConnect` | `companion_boot.connectNow` falling back to `Registry.addServer`. | -| `SkipConsumer`, `Teardown` | `connectCompanion`'s deferred `discard`, and `Registry.deinitAtExit` calling `discard` (cancel, join, tear down); `Registry.deinit` sets `retry_abort` before `joinPending`. | +| `KeysOk`, `FatalExit`, `BannerOk` | `startup.resolveKeysOrRunAcpPreAuth` and `session_start.setupWorktreeAndBanner`. Both can leave through `std.process.fatal` (no credential, `--add-dir`, isolation, `--experiment`), which runs no defer and joins nothing. | +| `Start` | `main` after `setupWorktreeAndBanner`: `boot_prewarm.startCompanion` (gate: `wantsCompanion`, no project `.mcp.json`, opt-out, PATH), then `defer boot_prewarm.discard(io)`. `start` launches `connectTask` with `io.concurrent`. | +| `WindowError` | Any `try` that fails between that start and `initRegistryConsent` returning: the deferred `discard` runs. | +| `RegistryUp` | `initRegistryConsent` returned; `defer registry_storage.deinitAtExit()` is armed. | +| `WorkerFinish`, `WorkerTimeout` | `connectTask`: one `startServer` on a task-local registry whose `spawn_watch` publishes every child (`Registry.spawnStdio`). A failure reaps its child; success hands the child to the outcome. A child that never answers ends the attempt only at the 15 s handshake timeout, or when `discard` stops it. | +| `MoveCwd` | Any chdir between the start and the consumer. Nothing in the code does this now that the start follows the banner; the cwd key is kept and checked. | +| `TakeAdopt`, `TakeFailed`, `TakeMismatch` + `MismatchJoin`, `TakeNone` | `boot_prewarm.take` (through `connect`): `matches` (cwd key, home, probe and lazy settings), `Future.await`, then `mcp_boot.adoptStarted` under the registry mutex. A failed attempt returns its error, the error `addServer` would have returned. A mismatch calls `discard` and the caller runs `addServer`. | +| `InlineConnect` | `boot_prewarm.connect` falling back to `Registry.addServer` when no attempt ran to a result. | +| `SkipConsumer`, `Teardown`, `DiscardJoin` | `connectCompanion`'s deferred `discard`, and `Registry.deinitAtExit` calling `discard`: `Watch.stop` (SIGKILL the published child; one spawned later is stopped as it is published), `Future.await`, tear the outcome down. Then `Registry.deinit` sets `retry_abort` before `joinPending`. | | `IoDown` | `main` returns; `std.process.Init` deinitializes the threaded `Io` after joining its workers. | | `AttemptEnds`, `PauseTimesOut`, `PauseAborted`, `RetryEnds` | `mcp_start.task` and `pauseBeforeRetry`: the pause is `Io.Event.waitTimeout` on the registry's heap-owned event. | -The positive configuration checks, over every interleaving of these actions: -the outcome is never used before the worker ended (`JoinedBeforeUse`), a -prewarm recorded in another directory is never adopted (`NoStaleAdoption`), -no worker or start task is live once the `Io` is down (`NoUseAfterTeardown`), -no prewarm-owned server outlives teardown (`NoLeakAtExit`), the companion is -registered at most once (`AtMostOneCompanion`), the consumer always ends -adopted or with the inline connect tried (`NeverMissing`), and no retry starts -after teardown signalled (`NoRetryAfterAbort`). Under weak fairness of the -worker, consumer, teardown and task steps, a started teardown finishes -(`TeardownFinishes`). +The positive configuration checks the following over every interleaving of +these actions, including a companion that never answers: + +- `ExitClean`: once the process has exited, no companion the prewarm spawned + is alive, whether prewarm-owned or registered, and no prewarm worker was + left unjoined. This is the real hazard of every exit path. +- `NoExitStall`: no exit path waits out a silent companion's handshake timeout. +- `JoinedBeforeUse`: the outcome is never used before the worker ended. +- `NoStaleAdoption`: a prewarm recorded in another directory is never adopted. +- `NoRepeatAfterResult`: an attempt that ran to a result is the one attempt. +- `AtMostOneCompanion`: the companion is registered at most once. +- `NeverMissing`: the consumer always ends adopted, with an attempt's result, + or with the inline connect tried. +- `NoRetryAfterAbort`: no retry starts after teardown signalled. +- `NoUseAfterTeardown`: no worker or start task is live once the `Io` is down. +- `TeardownFinishes`: under weak fairness of the worker (including its + timeout), consumer, teardown and task steps, every exit that started + finishes. + +`NoUseAfterTeardown` is not evidence about this change. `std.process.Init` +joins every worker before it deinitializes the `Io`, so the runtime rules +that hazard out by construction. Its control removes that join from the +model. On the uncovered exit paths the actual failure was a stall plus a +leaked child, which `ExitClean` and `NoExitStall` capture. `ExitClean` +replaces the earlier `NoLeakAtExit`, which covered only the normal teardown. Run TLC from `formal/` with `-deadlock`, since exit is a terminal state. @@ -39,19 +57,38 @@ Run TLC from `formal/` with `-deadlock`, since exit is a terminal state. | `BootPrewarmNoCwdKey.cfg` | Adopt without comparing the cwd key | `NoStaleAdoption` | | `BootPrewarmNoTeardownJoin.cfg` | Tear the `Io` down without joining the prewarm or the start task | `NoUseAfterTeardown` | | `BootPrewarmNoRetryAbort.cfg` | Ignore `retry_abort` in the pause | `NoRetryAfterAbort` | +| `BootPrewarmEarlyStart.cfg` | Start the prewarm before credentials resolve (the first revision): a credential failure exits fatally with the child alive | `ExitClean` | +| `BootPrewarmNoWindowDiscard.cfg` | No deferred discard between the start and the registry: an error return leaves the connected child behind | `ExitClean` | +| `BootPrewarmNoStop.cfg` | `discard` only joins, without stopping the child: an exit waits for the handshake timeout | `NoExitStall` | +| `BootPrewarmRetryFailed.cfg` | A failed attempt falls back to a second, inline start (the first revision) | `NoRepeatAfterResult` | + +These are mutation checks of the model. Two of them, `EarlyStart` and +`RetryFailed`, are also the behavior of the first revision of the prewarm. +Runtime anchors that fail on that revision: + +- `boot prewarm: a failing companion is started exactly as often as the + inline path starts it`: two spawns against one on the first revision. +- `boot prewarm: an error return before the registry exists discards promptly + and leaves no child`. On a `discard` that cancels the worker, the child + survives or the test process faults: a cancel delivered inside the bounded + handshake's `Io.Select` returns before the select's read task stops. On a + `discard` that only joins, it waits for the 15 s handshake timeout. +- `src/mcp_start.zig` `teardown with a pending transient retry returns in well + under the pause` fails when `Registry.deinit` does not set the event. + +`boot prewarm: main starts it after credentials and the banner, and defers its +discard` checks only source placement, which is weaker evidence. The no-key +case was also checked end to end with a stub companion that ignores stdin: +the first revision left it running after `graff -p` exited 1, and the current +code spawns nothing. + +Omitted: + +- the handshake's own protocol states; +- the content of the catalog (the byte-comparison test covers it); +- a fatal exit after the registry exists (it does not involve the prewarm); +- Windows, where `Watch` has no signal and `discard` waits for the attempt, + bounded by the handshake timeout; +- more than one start task. -These are mutation checks of the model, not historical regressions. The -retry pause has a runtime anchor that fails on the old revision: -`src/mcp_start.zig` `teardown with a pending transient retry returns in well -under the pause` (fails when `Registry.deinit` does not set the event). The -prewarm anchors are the `boot prewarm:` tests in `src/boot_prewarm.zig` -(byte-identical catalog against `addServer`, cwd-change discard, settings -mismatch, failed-prewarm fallback, discard of a silent server, and the -early-exit gate). - -Omitted: the handshake's own protocol states and timeouts, the content of the -catalog (covered by the byte-comparison test), the error-return paths that -leave `main` before the registry exists (there `std.process.Init` joins the -worker, bounded by the handshake timeout, and process exit closes the child's -pipe), and more than one start task. The model does not establish that the -Zig code refines it. +The model does not establish that the Zig code refines it. diff --git a/formal/change-evidence.md b/formal/change-evidence.md index e83bd1ad0..3ef96c5ec 100644 --- a/formal/change-evidence.md +++ b/formal/change-evidence.md @@ -31,7 +31,10 @@ For each behavioral change, retain this chain: | Permission replies belong to their live transport and session | ACP permission extension | Client-side evidence moved with the desktop app to Harness | | A companion prewarm is joined before its result is used | `JoinedBeforeUse` | `src/boot_prewarm.zig`: `boot prewarm: the adopted companion matches the inline addServer catalog byte for byte` | | A prewarm started in another cwd is never adopted | `NoStaleAdoption` | `src/boot_prewarm.zig`: `boot prewarm: a cwd change between start and consumer discards it and the inline path runs` | -| No prewarm worker outlives teardown | `NoUseAfterTeardown` | `src/boot_prewarm.zig`: `boot prewarm: discard joins a pending handshake and tears it down` | +| No prewarm worker outlives teardown (the runtime's join guarantees this by construction; see boot-prewarm.md) | `NoUseAfterTeardown` | `src/boot_prewarm.zig`: `boot prewarm: discard joins a pending handshake and tears it down` | +| No exit path leaves a prewarm-spawned companion or an unjoined worker: the prewarm starts after credentials and the banner, and main defers its discard | `ExitClean` (controls `BootPrewarmEarlyStart.cfg`, `BootPrewarmNoWindowDiscard.cfg`) | `src/boot_prewarm.zig`: `boot prewarm: an error return before the registry exists discards promptly and leaves no child`, `boot prewarm: main starts it after credentials and the banner, and defers its discard` (source placement only) | +| A discard stops the worker's child instead of cancelling the worker, so no exit waits out a silent companion's handshake | `NoExitStall` (control `BootPrewarmNoStop.cfg`) | `src/boot_prewarm.zig`: `boot prewarm: an error return before the registry exists discards promptly and leaves no child` (fails on the cancel-based discard: the child survives or the process faults) | +| A prewarm attempt that ran to a result is the one attempt, its error included | `NoRepeatAfterResult` (control `BootPrewarmRetryFailed.cfg`) | `src/boot_prewarm.zig`: `boot prewarm: a failing companion is started exactly as often as the inline path starts it` | | Teardown ends a retry pause and the retry does not run | `NoRetryAfterAbort` | `src/mcp_start.zig`: `teardown with a pending transient retry returns in well under the pause` | These anchors connect intent and implementation tests. They do not establish diff --git a/scripts/check-formal.py b/scripts/check-formal.py index 1fe569ec8..020e7c19b 100644 --- a/scripts/check-formal.py +++ b/scripts/check-formal.py @@ -35,6 +35,10 @@ ("BootPrewarm.tla", "BootPrewarmNoCwdKey.cfg", "NoStaleAdoption"), ("BootPrewarm.tla", "BootPrewarmNoTeardownJoin.cfg", "NoUseAfterTeardown"), ("BootPrewarm.tla", "BootPrewarmNoRetryAbort.cfg", "NoRetryAfterAbort"), + ("BootPrewarm.tla", "BootPrewarmEarlyStart.cfg", "ExitClean"), + ("BootPrewarm.tla", "BootPrewarmNoWindowDiscard.cfg", "ExitClean"), + ("BootPrewarm.tla", "BootPrewarmNoStop.cfg", "NoExitStall"), + ("BootPrewarm.tla", "BootPrewarmRetryFailed.cfg", "NoRepeatAfterResult"), ) diff --git a/src/boot_prewarm.zig b/src/boot_prewarm.zig index 46033011c..f0b204820 100644 --- a/src/boot_prewarm.zig +++ b/src/boot_prewarm.zig @@ -11,13 +11,16 @@ //! the only server that can connect and the adopted outcome fills exactly the //! slot `addServer` would have. Every other case keeps the serial path. //! -//! One slot, touched only by the boot thread: `start` after `runSubcommand`, -//! `take` in `connectCompanion` (await, then merge under the registry mutex, -//! ADR 0197), `discard` (cancel, join, tear down) when it is not adopted and -//! at registry teardown. A cwd or registry-setting mismatch discards; a quick -//! failure falls back to the inline `addServer`, a timeout does not repeat. -//! The worker owns copies of everything it reads: `std.process.Init` frees -//! `environ_map` before it joins worker threads. +//! One slot, touched only by the boot thread: `start` in `main` once +//! credentials resolved and the banner settled the cwd (a run that exits +//! there spawns nothing, as before), `take` in `connectCompanion` (await, +//! then merge under the registry mutex, ADR 0197), `discard` (stop, join, +//! tear down) when it is not adopted, on any return before the registry's +//! teardown, and at that teardown. A cwd or registry-setting mismatch +//! discards and the inline `addServer` runs; an attempt that ran to a result +//! is the one attempt, its error included. The worker owns copies of +//! everything it reads: `std.process.Init` frees `environ_map` before it +//! joins worker threads. const std = @import("std"); const Io = std.Io; @@ -49,12 +52,62 @@ const Slot = struct { future: Io.Future(Joined), owned: std.heap.ArenaAllocator, spec: Spec, + watch: *Watch, cwd: [std.fs.max_path_bytes]u8 = undefined, cwd_len: usize = 0, }; var slot: ?Slot = null; +/// The worker's live child, so `discard` can end a handshake in progress by +/// stopping the child: the read sees end of stream, the attempt fails and +/// cleans up on its own, and `discard` joins it. The worker is never +/// cancelled: a cancel that lands inside the bounded handshake's `Io.Select` +/// returns before the select's read task stops, and that task outlives its +/// frame. The worker publishes each child it spawns (`Registry.spawnStdio`) +/// and forgets it before it reaps it, both under `lock`, so `stop` only ever +/// signals a child nobody has waited on: its pid cannot have been reused. +pub const Watch = struct { + lock: std.atomic.Mutex = .unlocked, + stopping: bool = false, + child: ?std.process.Child.Id = null, + + fn acquire(w: *Watch) void { + while (!w.lock.tryLock()) std.atomic.spinLoopHint(); + } + + /// Worker, right after a spawn. A child spawned after `stop` is stopped at once. + pub fn publish(w: *Watch, id: ?std.process.Child.Id) void { + w.acquire(); + defer w.lock.unlock(); + w.child = id; + if (w.stopping) w.signal(); + } + + /// Worker, before it reaps the child or hands it to the consumer. + pub fn forget(w: *Watch) void { + w.acquire(); + defer w.lock.unlock(); + w.child = null; + } + + /// Boot thread: end the attempt now. + fn stop(w: *Watch) void { + w.acquire(); + defer w.lock.unlock(); + w.stopping = true; + w.signal(); + } + + /// Windows has no signal here: `discard` then waits for the attempt, + /// which the handshake timeout bounds. + fn signal(w: *Watch) void { + const id = w.child orelse return; + if (@import("builtin").os.tag == .windows) return; + std.posix.kill(id, .KILL) catch {}; + } +}; + /// Settings that only `Registry.init` reads. With any of them set the /// prewarm could not match the registry, so it does not start. const registry_env = [_][]const u8{ "GRAFF_MCP_PROBE", "GRAFF_MCP_EAGER", "GRAFF_MCP_HANDSHAKE_SECS", "GRAFF_MCP_PROBE_MS" }; @@ -69,8 +122,9 @@ pub fn wantsCompanion(flags: args.Flags, environ_map: anytype) bool { return true; } -/// Called once boot knows this is a session (after `runSubcommand`). Picks -/// the companion `connectCompanion` would try first and starts it. +/// Called once credentials resolved and the cwd is final (after +/// `setupWorktreeAndBanner`). Picks the companion `connectCompanion` would +/// try first and starts it. The caller defers `discard`. pub fn startCompanion(io: Io, gpa: Allocator, environ_map: anytype, flags: args.Flags) void { if (!wantsCompanion(flags, environ_map)) return; if (Io.Dir.cwd().access(io, ".mcp.json", .{})) |_| return else |_| {} // project servers: not the empty registry @@ -116,10 +170,15 @@ pub fn start(io: Io, gpa: Allocator, spec: Spec) void { owned.deinit(); return; }; - var s: Slot = .{ .future = undefined, .owned = owned, .spec = copy }; + const watch = owned.allocator().create(Watch) catch { + owned.deinit(); + return; + }; + watch.* = .{}; + var s: Slot = .{ .future = undefined, .owned = owned, .spec = copy, .watch = watch }; s.cwd_len = cwdKey(io, &s.cwd); // Never `io.async`: run inline it would serialize the boot again. - s.future = io.concurrent(connectTask, .{ gpa, io, copy }) catch { + s.future = io.concurrent(connectTask, .{ gpa, io, copy, watch }) catch { s.owned.deinit(); return; }; @@ -143,16 +202,17 @@ fn cwdKey(io: Io, buf: *[std.fs.max_path_bytes]u8) usize { /// The worker: `addServer`'s connect on a task-local registry (the pattern /// `mcp_start.attempt` uses), one attempt, no retry, no failure record. -fn connectTask(gpa: Allocator, io: Io, spec: Spec) Joined { +fn connectTask(gpa: Allocator, io: Io, spec: Spec, watch: *Watch) Joined { var arena_state = std.heap.ArenaAllocator.init(gpa); - const outcome = connectInto(gpa, io, spec, &arena_state) catch |err| { + const outcome = connectInto(gpa, io, spec, watch, &arena_state) catch |err| { arena_state.deinit(); return .{ .err = err }; }; + watch.forget(); // the outcome owns the child now return .{ .outcome = outcome }; } -fn connectInto(gpa: Allocator, io: Io, spec: Spec, arena_state: *std.heap.ArenaAllocator) !mcp_boot.StartOutcome { +fn connectInto(gpa: Allocator, io: Io, spec: Spec, watch: *Watch, arena_state: *std.heap.ArenaAllocator) !mcp_boot.StartOutcome { const a = arena_state.allocator(); var tmp: mcp.Registry = .{ .gpa = gpa, @@ -162,6 +222,7 @@ fn connectInto(gpa: Allocator, io: Io, spec: Spec, arena_state: *std.heap.ArenaA .stdio_probe = spec.stdio_probe, .lazy_stdio = spec.lazy_stdio, .show_diagnostics = spec.show_diagnostics, + .spawn_watch = watch, }; defer tmp.arena_state.deinit(); // Same object, same key order as `Registry.addServer`: the cache key and @@ -177,9 +238,18 @@ fn connectInto(gpa: Allocator, io: Io, spec: Spec, arena_state: *std.heap.ArenaA return .{ .server = servers.items[0], .tools = tools.items, .arena_state = arena_state.* }; } -/// The consumer. Null: nothing usable for `name` (none started, or it was -/// discarded) — connect inline as before. Otherwise the joined result: the -/// server merged into `reg`, or the error `addServer` would have returned. +/// `addServer(name, command, args)` for the consumer, served by the prewarm +/// when it ran this companion: one start either way. +pub fn connect(reg: *mcp.Registry, name: []const u8, command: []const u8, argv: []const []const u8) !void { + if (take(reg, name)) |joined| return joined; + _ = try reg.addServer(name, command, argv); +} + +/// The consumer. Null: no attempt ran to a result for `name` (none started, +/// or discarded on a mismatch) — connect inline as before. +/// Otherwise the joined result: the server merged into `reg`, or the error +/// that attempt ended with, which is the error `addServer` would have +/// returned. A failed attempt is not repeated: the serial path made one. pub fn take(reg: *mcp.Registry, name: []const u8) ?anyerror!void { const s = if (slot) |*s| s else return null; if (!std.mem.eql(u8, s.spec.name, name)) return null; @@ -189,7 +259,7 @@ pub fn take(reg: *mcp.Registry, name: []const u8) ?anyerror!void { } const joined = s.future.await(reg.io); release(); - if (joined.err) |err| return if (repeatInline(err)) null else err; + if (joined.err) |err| return err; if (!mcp_boot.adoptStarted(reg, joined.outcome)) return error.McpServerAlreadyConnected; return {}; } @@ -202,21 +272,12 @@ fn matches(reg: *const mcp.Registry, s: *const Slot) bool { return n > 0 and std.mem.eql(u8, now[0..n], s.cwd[0..s.cwd_len]); } -/// A prewarm that failed fast is retried on the boot thread, which is what -/// the serial path did at this point. A timeout already took the full -/// handshake budget; repeating it would double the worst case. -fn repeatInline(err: anyerror) bool { - return switch (err) { - error.McpHandshakeTimeout, error.McpRequestTimedOut, error.Canceled => false, - else => true, - }; -} - -/// Cancel, join and tear down a prewarm nobody adopted. No-op when the slot +/// Stop, join and tear down a prewarm nobody adopted. No-op when the slot /// is empty, so every exit path may call it. pub fn discard(io: Io) void { const s = if (slot) |*s| s else return; - var joined = s.future.cancel(io); + s.watch.stop(); + var joined = s.future.await(io); release(); if (joined.outcome.server) |server| mcp_rpc.deinitServer(server, io, .init(io, mcp_teardown.teardown_grace)); if (joined.outcome.arena_state) |*ta| ta.deinit(); @@ -313,17 +374,104 @@ test "boot prewarm: a registry with other settings is not adopted" { try testing.expectEqual(@as(usize, 0), reg.tools.len); } -test "boot prewarm: a failed prewarm falls back to the inline connect, never to a missing tool" { +/// A scratch directory with a spawn log the fixtures append their pid to. +const SpawnLog = struct { + tmp: testing.TmpDir, + path: []u8, + + fn init() !SpawnLog { + var tmp = testing.tmpDir(.{}); + errdefer tmp.cleanup(); + const dir = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator); + defer testing.allocator.free(dir); + return .{ .tmp = tmp, .path = try std.fmt.allocPrint(testing.allocator, "{s}/spawns", .{dir}) }; + } + + fn deinit(l: *SpawnLog) void { + testing.allocator.free(l.path); + l.tmp.cleanup(); + } + + fn pids(l: *const SpawnLog, buf: []std.posix.pid_t) []std.posix.pid_t { + var raw: [512]u8 = undefined; + const data = l.tmp.dir.readFile(testing.io, "spawns", &raw) catch return buf[0..0]; + var n: usize = 0; + var it = std.mem.tokenizeAny(u8, data, " \n"); + while (it.next()) |w| : (n += 1) { + if (n == buf.len) break; + buf[n] = std.fmt.parseInt(std.posix.pid_t, w, 10) catch return buf[0..n]; + } + return buf[0..n]; + } +}; + +fn processAlive(pid: std.posix.pid_t) bool { + std.posix.kill(pid, @fromBackingInt(@intCast(0))) catch |err| return err != error.ProcessNotFound; + return true; +} + +/// `sh -c