Skip to content

Commit 0e2789a

Browse files
colbymchenryclaude
andauthored
docs(agent-eval): nested MCP attach is startup-latency, not a hard block (colbymchenry#735)
Corrects the "run non-nested only" conclusion from colbymchenry#734. The codegraph server is healthy (handshake ~165ms); the flakiness is that on a multi-step implementation task the agent dives into Read/grep before codegraph finishes its ~2-3s startup (worse under nested CPU contention), so it runs with no codegraph. Fix: pre-warm a persistent daemon (high idle timeout) + skip the startup re-exec (CODEGRAPH_WASM_RELAUNCHED=1) so claude connects before the agent's first turn. claude's init snapshot can show status:"pending" even when it then connects — judge by actual codegraph usage, not the init line. ab-new-vs-baseline.sh now bakes in the pre-warm + skip-re-exec. Validated: a clean A/B showed the new build's agent used codegraph 2x / 5 Reads vs the baseline's 0 / 8 on the same fully-implemented task. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 28c5268 commit 0e2789a

2 files changed

Lines changed: 33 additions & 19 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ For each **language × framework**, validate on **small, medium, and large** rea
137137
1. **Pick the canonical flow** for the framework ("how does X reach Y": state→render, request→handler→view, query→SQL, action→reducer→store…).
138138
2. **Deterministic probes** (`scripts/agent-eval/probe-{node,explore}.mjs` against the built `dist/`): `codegraph_explore` with the flow's symbol names connects from→to end-to-end with no break (its Flow section shows the path); **no node explosion** (`select count(*) from nodes` stable before/after re-index); synthesized-edge **precision** spot-check (`select … where provenance='heuristic'`).
139139
3. **Agent A/B** (`scripts/agent-eval/run-all.sh <repo> "<Q>"`): with vs without codegraph, **≥2 runs/arm** (run-to-run variance is large — never conclude from n=1). Record **duration, total tool calls, Read, Grep**. Optional forced-Read-0 sufficiency proof via the block-read hook (`scripts/agent-eval/hook-settings.json`).
140-
- **Run agent-evals in a REAL terminal — NEVER nested inside a Claude Code session** (don't spawn `claude -p` from a Bash tool call). The codegraph MCP server is healthy (full `initialize``tools/list` handshake ~165ms, daemon and in-process modes both fine), but a nested `claude -p` marks it `status:"pending"` / 0 tools under CPU/timing contention and the agent silently runs with no codegraph — it can connect early in a session, then degrade to consistent failure as nested spawns pile up. `CODEGRAPH_NO_DAEMON=1` and `< /dev/null` do NOT fix it (it's the nested client, not the server). Confirm via `parse-run.mjs` (`codegraph tools exposed: 0` = void run). To isolate a change — **new-build vs baseline-build, both codegraph-on** (vs run-all.sh's with-vs-without) — use `scripts/agent-eval/ab-new-vs-baseline.sh <indexed-repo> "<task>" [baseline-ref]`.
140+
- **MCP attach is a startup-latency issue, not a hard block.** On a multi-step task the agent dives into Read/grep before codegraph finishes its ~2-3s startup (worse when the eval is itself run nested inside a Claude session, under CPU contention), so it runs with no codegraph. Fix: **pre-warm a persistent daemon** for the target (`CODEGRAPH_DAEMON_IDLE_TIMEOUT_MS` high; spawn `serve --mcp --path <target> </dev/null &`; wait for `.codegraph/daemon.sock`) **and skip the startup re-exec** (`CODEGRAPH_WASM_RELAUNCHED=1`) so claude connects before the agent's first turn. Don't trust claude's `init` snapshot — it can read `status:"pending"` / 0 tools even when it then connects; judge by actual codegraph usage in `parse-run.mjs`'s `by type`. To isolate a change — **new-build vs baseline-build, both codegraph-on** (vs run-all.sh's with-vs-without) — use `scripts/agent-eval/ab-new-vs-baseline.sh <indexed-repo> "<task>" [baseline-ref]` (it bakes in the pre-warm).
141141
4. **Pass bar:** a normal flow question reaches **~0 Read/Grep within the repo's explore-call budget**, runs **faster** than without-codegraph, and shows **no regression on a control repo**. Record the numbers in `docs/design/dynamic-dispatch-coverage-playbook.md` (the coverage matrix).
142142

143143
Full playbook + per-mechanism design: `docs/design/dynamic-dispatch-coverage-playbook.md` and `docs/design/callback-edge-synthesis.md`.

scripts/agent-eval/ab-new-vs-baseline.sh

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22
# A/B a codegraph retrieval/steering change: the NEW build (current HEAD) vs a
33
# BASELINE build (a git ref) — BOTH with codegraph attached — on the same
44
# implementation task, measuring how many Read vs codegraph calls the agent
5-
# makes. This ISOLATES the change (unlike run-all.sh, which is with-vs-without
6-
# codegraph). The agent works on a throwaway copy of the target, so its edits
7-
# never touch your repos.
5+
# makes. ISOLATES the change (unlike run-all.sh's with-vs-without). The agent
6+
# works on a throwaway copy of the target, so your repos are never touched.
87
#
9-
# *** RUN THIS IN A REAL TERMINAL — NOT nested inside a Claude Code session. ***
10-
# A `claude -p` spawned from within another Claude session (e.g. from a Bash
11-
# tool call) cannot reliably attach the codegraph MCP server: the server is
12-
# healthy (full handshake ~165ms) but the nested client marks it
13-
# status:"pending" / 0 tools under CPU/timing contention, and degrades to
14-
# consistent failure over a long session. NO_DAEMON + `< /dev/null` do NOT fix
15-
# it — it's the nested client, not the server. See codegraph/CLAUDE.md
16-
# ("Running agent-evals — do NOT nest").
8+
# Reliable attach (works even when this is itself run nested inside a Claude
9+
# session): each arm PRE-WARMS a persistent codegraph daemon for its target so
10+
# claude connects to an already-bound, index-loaded daemon instantly — before
11+
# the agent's first turn — and SKIPS codegraph's startup re-exec via
12+
# CODEGRAPH_WASM_RELAUNCHED=1. Without this, on a multi-step task the agent
13+
# dives into Read/grep before codegraph finishes its ~2-3s startup (worse under
14+
# the CPU contention of a nested run) and runs with NO codegraph.
15+
#
16+
# Gotcha: claude's `system/init` snapshot can read status:"pending" / 0 tools
17+
# even when the server then connects fine — judge by ACTUAL codegraph usage in
18+
# parse-run.mjs's "by type", not the init line.
1719
#
1820
# Usage: ab-new-vs-baseline.sh <indexed-repo> "<task>" [baseline-ref]
1921
# <indexed-repo> a repo with a .codegraph index (copied per arm)
@@ -38,9 +40,13 @@ fi
3840
CHANGED=$(git -C "$ENGINE" diff --name-only "$BASE_REF" HEAD -- src 2>/dev/null)
3941
[ -n "$CHANGED" ] || { echo "no src/ changes between $BASE_REF and HEAD — nothing to A/B"; exit 1; }
4042

41-
# Always restore the engine to HEAD on exit, even if interrupted mid-arm.
42-
restore() { git -C "$ENGINE" checkout HEAD -- $CHANGED 2>/dev/null; ( cd "$ENGINE" && npm run build >/dev/null 2>&1 ); }
43-
trap restore EXIT
43+
# On exit: kill any eval daemons + restore the engine to HEAD.
44+
cleanup() {
45+
pkill -9 -f "serve --mcp --path $OUT/" 2>/dev/null
46+
git -C "$ENGINE" checkout HEAD -- $CHANGED 2>/dev/null
47+
( cd "$ENGINE" && npm run build >/dev/null 2>&1 )
48+
}
49+
trap cleanup EXIT
4450

4551
mkdir -p "$OUT"
4652
echo "###### engine=$ENGINE baseline=$BASE_REF"
@@ -54,17 +60,25 @@ rm -rf "$OUT/t-new" "$OUT/t-base"
5460
rsync -a --exclude node_modules --exclude .git --exclude dist --exclude .codegraph "$TARGET/" "$OUT/t-new/"
5561
cp -R "$OUT/t-new" "$OUT/t-base"
5662

57-
cfg() { printf '{"mcpServers":{"codegraph":{"command":"%s","args":["serve","--mcp","--path","%s"]}}}' "$BIN" "$1" > "$2"; }
63+
prewarm() { # target — spawn a persistent daemon (current $BIN) and wait for its socket
64+
pkill -9 -f "serve --mcp --path $1" 2>/dev/null
65+
CODEGRAPH_DAEMON_IDLE_TIMEOUT_MS=1800000 node "$BIN" serve --mcp --path "$1" </dev/null >/dev/null 2>&1 &
66+
node -e 'const fs=require("fs");let n=0;const t=setInterval(()=>{if(fs.existsSync(process.argv[1]+"/.codegraph/daemon.sock")){clearInterval(t);process.exit(0)}if(n++>150){clearInterval(t);process.exit(1)}},100)' "$1" \
67+
&& echo " daemon warm: $1" || echo " WARN: daemon never bound for $1 (arm may run without codegraph)"
68+
}
5869

5970
run_arm() { # label, target-copy
6071
local label="$1" tgt="$2" c="$OUT/mcp-$1.json"
61-
cfg "$tgt" "$c"
72+
# Connect to the pre-warmed daemon; skip the startup re-exec for a fast attach.
73+
printf '{"mcpServers":{"codegraph":{"command":"env","args":["CODEGRAPH_WASM_RELAUNCHED=1","node","%s","serve","--mcp","--path","%s"]}}}' "$BIN" "$tgt" > "$c"
74+
prewarm "$tgt"
6275
echo "############## ARM [$label] ##############"
6376
( cd "$tgt" && claude -p "$TASK" \
6477
--output-format stream-json --verbose --permission-mode bypassPermissions \
6578
--model opus --max-budget-usd 4 --strict-mcp-config --mcp-config "$c" \
66-
< /dev/null > "$OUT/run-$label.jsonl" 2>"$OUT/run-$label.err" )
67-
node "$PARSE" "$OUT/run-$label.jsonl" 2>&1 | grep -E "tools exposed|by type|Result" || echo " (parse failed — see $OUT/run-$label.jsonl)"
79+
</dev/null > "$OUT/run-$label.jsonl" 2>"$OUT/run-$label.err" )
80+
node "$PARSE" "$OUT/run-$label.jsonl" 2>&1 | grep -E "by type|Result" || echo " (parse failed — see $OUT/run-$label.jsonl)"
81+
pkill -9 -f "serve --mcp --path $tgt" 2>/dev/null
6882
echo
6983
}
7084

0 commit comments

Comments
 (0)