Conversation
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
This PR updates the benchmark harness scripts to run against a harness-owned daemon runtime and cache, preventing benchmark runs from contaminating or depending on the operator’s live store/daemon state, and adds a contract test to enforce that isolation.
Changes:
- Add a benchmark runtime isolation contract test that verifies both harnesses don’t leak caller
CBM_*dirs into product processes and thatbenchmark-index.shwrites new timing artifacts. - Update
benchmark-index.shto initialize/cleanup a private runtime, start a private daemon before timing, and emitsetup-time.txtandtotal-time.txt. - Update
benchmark-search-graph.shto accept<repo-path>, index into a private cache (untimed), and run queries against a warm private daemon.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tests/test_benchmark_runtime_isolation_contract.sh | Adds an isolation contract test + timing file presence checks for benchmark harnesses. |
| scripts/test.sh | Wires the new contract test into the shell test runner. |
| scripts/benchmark-search-graph.sh | Switches to repo-path input and makes indexing/queries use a harness-owned runtime/daemon. |
| scripts/benchmark-index.sh | Runs index benchmark under harness-owned runtime/cache, starts private daemon before timing, and writes setup/total timing files. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| echo "private daemon did not start" >&2 | ||
| exit 1 | ||
| fi | ||
| INDEX_JSON=$("$BINARY" cli index_repository "{\"repo_path\":\"$REPO\",\"mode\":\"full\"}" 2>/dev/null || echo '{}') |
There was a problem hiding this comment.
Taken, in both scripts (8f0ff31): REPO_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$REPO"), the spelling scripts/soak-test.sh already uses for the same reason.
benchmark-index.sh carried the identical unescaped interpolation (pre-existing, not flagged); fixing one file and leaving its twin looked worse than the small extra diff.
Fail-before is in the contract now: with a repository path containing " and \, the old request came out as {"repo_path":"/private/tmp/.../re"po\dir","mode":"full"}, which is not JSON. The test asserts the recorded request parses and that its repo_path is the resolved repository. POSIX-only case — NTFS rejects both characters in a path component.
| # alone. setup-time.txt keeps the activation cost attributable and | ||
| # total-time.txt is their sum — the figure comparable with earlier runs, which | ||
| # paid activation inside the index timing whenever no daemon was already warm. | ||
| SETUP_START_MS=$(python3 -c "import time; print(int(time.time()*1000))") |
There was a problem hiding this comment.
Taken (8f0ff31). All three readings had to move together — they are subtracted from each other, so mixing clocks was not an option — through one helper, bench_now_ms, using time.monotonic_ns() // 1000000. Its reference point is fixed per boot on every platform this runs on, so the three readings stay comparable even though each comes from its own python3 process.
| fi | ||
|
|
||
| # Index via CLI and capture timing | ||
| START_MS=$(python3 -c "import time; print(int(time.time()*1000))") |
There was a problem hiding this comment.
Taken (8f0ff31). All three readings had to move together — they are subtracted from each other, so mixing clocks was not an option — through one helper, bench_now_ms, using time.monotonic_ns() // 1000000. Its reference point is fixed per boot on every platform this runs on, so the three readings stay comparable even though each comes from its own python3 process.
| INDEX_JSON=$("$BINARY" cli index_repository "{\"repo_path\":\"$REPO\",\"mode\":\"full\"}" 2>/dev/null || echo '{}') | ||
| PROJECT=$(echo "$INDEX_JSON" | python3 -c " | ||
| import json, sys | ||
| d = json.load(sys.stdin) | ||
| if 'content' in d: | ||
| d = json.loads(d['content'][0]['text']) | ||
| print(d.get('project', '')) | ||
| " 2>/dev/null || echo "") | ||
| if [ -z "$PROJECT" ]; then | ||
| echo "index of $REPO did not report a project" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
There was a problem hiding this comment.
Taken (8f0ff31). Both the index call and the parse now write stderr to a run-private temp file (removed by the same EXIT trap), and the failure path prints that stderr plus the first 500 bytes of the response under the existing "did not report a project" line. Covered in the contract: the fixture refuses the index with a stderr line, and the test requires both the section header and the cause to reach the output — it fails against the previous head of this PR.
As of 07ee617 benchmark-index.sh keeps the index stderr the same way: the maintainer's round asked for a refused index to exit non-zero, and a failure that names only its symptom would have been half of it.
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
|
Thank you for the benchmark isolation work. The evaluation plan has a later graph phase that expects the indexed data to remain available, so cleanup and retained-runtime ownership need an explicit handoff before this is integrated. We need more time to review that lifecycle contract and keep the reported total-time comparison meaningful; no benchmark redesign is being accepted here. |
scripts/benchmark-index.sh and scripts/benchmark-search-graph.sh ran the product with no runtime or cache of their own: the benchmark repository was indexed into the operator's live store and every one-shot joined the operator's account daemon. Source scripts/test-runtime.sh in both, start the private daemon before timing, record setup-time.txt and total-time.txt beside index-time.txt, and index from a repository path instead of querying a project in the live store. Add tests/test_benchmark_runtime_isolation_contract.sh, which fails before this change, and wire it into scripts/test.sh. Part of DeusData#1696. Signed-off-by: Anton Standrik <astandrik@yandex-team.ru>
Review follow-up for the benchmark harness isolation. - Both benchmark scripts build the index request with a JSON-escaped repository path (python3 json.dumps, the spelling soak-test.sh already uses): a path containing a quote or a backslash produced a payload the server could not parse. - benchmark-index.sh reads its three timestamps from one helper backed by time.monotonic_ns(); an NTP step mid-run no longer skews or negates a figure whose purpose is comparison across runs. - benchmark-search-graph.sh keeps the stderr of the index call and of the response parse in a run-private temp file (removed by the EXIT trap) and prints it, plus the first 500 bytes of the response, under the existing "did not report a project" line. - The contract test now refuses the index from its fixture and requires the cause to reach the output, and (POSIX only) drives a repository path with a quote and a backslash and requires the recorded request to parse as JSON with the resolved path. Both assertions fail against the previous head of this branch. Signed-off-by: Anton Standrik <astandrik@yandex-team.ru>
The evaluation plan (docs/EVALUATION_PLAN.md §7) indexes a language with benchmark-index.sh and then answers graph questions against that index from its own MCP session. With a run-private runtime (DeusData#1696) the index was gone before that session could start, so the handoff is now explicit and opt-in. With CBM_BENCH_KEEP_RUNTIME set, a SUCCESSFUL run stops its daemon, leaves the private root in place and records the paths that reach it in <results>/<lang>/runtime-root.txt (sourceable: CBM_BENCH_RUNTIME_ROOT, CBM_RUNTIME_DIR, CBM_CACHE_DIR). Ownership of that root, including its removal, passes to the caller. A failed run cleans up regardless: there is no index worth keeping and nothing may leak. Nothing changes in the default path or in the three timing files. §7's skeleton sets the flag, sources the file before the graph session and removes the root in step 8 instead of the live-store *.db files. The contract test drives a kept run and requires the root to survive, the handoff file to be sourceable, and its paths to be the ones the product processes actually used; the unflagged run still asserts the root is gone. The kept-run assertions fail against the previous head. Signed-off-by: Anton Standrik <astandrik@yandex-team.ru>
b5b69af to
9574c31
Compare
DeusData
left a comment
There was a problem hiding this comment.
Thank you for the three follow-up commits — this answers the handoff question exactly the way I hoped: opt-in only, the daemon stopped before the root is handed over, a sourceable runtime-root.txt so the graph session never has to guess paths, and ownership stated in one sentence. I verified it on the merge result with today's main (e783f73d): merge clean; the contract test passes as Step 0i2 in 1.2 s; with either benchmark script alone restored to main it fails on the right assertion (benchmark-index exposed the caller CBM_RUNTIME_DIR to a product process, and the search-graph twin), so it binds to both fixes; bash -n and shellcheck -x -P scripts clean on every touched script; no delay added anywhere; and the caller's runtime and cache never reach a product process, with zero cbmrt.* roots left behind after every run. The scripts/test.sh hunk is one purely additive step, no environment or ordering change, and I have noted its scope on our side.
One thing to close before it merges, and it is in the contract you wrote rather than in the isolation: retention keys on the script's exit status, but an index failure is swallowed. bench_finish keeps the root when $? -eq 0, yet the index call is … || echo '{"error":"index failed"}', so a refused index still exits 0. With CBM_BENCH_KEEP_RUNTIME=1 that keeps the root and writes runtime-root.txt for an index that does not exist — reproduced here: exit 0, 00-index.json = {"error":"index failed"}, root present, handoff written. That contradicts the comment above bench_finish ("a failed run cleans up regardless: there is no index worth keeping") and it is the case §7 would hit hardest, because the graph session would then run against an empty store and every question would look like a miss. The contract test enshrines exactly this path: the keep run uses the probe fixture, which refuses cli index_repository, so the assertions at the keep step check that a root is kept after a failed index.
What I would ask for, one small round:
- Make retention depend on the index having succeeded, not on the exit status — either require a non-empty project from the index in
bench_finish, or let an index failure exit non-zero after the timing files are written (my preference: the second, so the caller's loop notices too). - In the contract test, let the keep-run fixture answer
cli index_repositorywith a minimal envelope (e.g.{"project":"probe"}) so the keep assertions cover the successful-index handoff, and add one assertion thatCBM_BENCH_KEEP_RUNTIME=1with a refused index leaves no root and no handoff file. - Two doc lines if you are in there anyway: §7 step 8 removes a root whose daemon the graph session (steps 4–7) may still have running — a
daemon stopunder the keptCBM_RUNTIME_DIRbelongs before therm -rf; and §13 Reproducibility still clears~/.cache/codebase-memory-mcp/*.dband calls the harness unflagged, which no longer describes what the harness does.
Incidental and not yours to fix: run_case in benchmark-search-graph.sh uses date +%s%3N, which BSD date on macOS does not support; the contract test never reaches it, so CI is unaffected, and I have recorded it on our side. The Windows guard failure on your run is the daemon endpoint cold-start race (#2275), not this diff; I will rerun it once that has landed. After this round the harness change is the one we want.
bench_finish keeps the private root on a zero exit status, but the index
call swallowed its failure (`|| echo '{"error":"index failed"}'`), so a
refused index exited 0 and, with CBM_BENCH_KEEP_RUNTIME set, kept an
empty root and wrote runtime-root.txt for an index that does not exist.
benchmark-index.sh now records the CLI's exit status, keeps 00-index.json
valid JSON on failure ({"error":"index failed","exit":N}), and exits 1
once every per-run file — the three timing files included — is written,
when the index failed or reported no project (the search-graph twin's own
test). bench_finish then cleans up as its comment always promised, and
the caller's loop notices. The index call keeps its stderr in a
run-private temp file, the twin's shape, and the failure prints it under
`--- index stderr ---` with the first 500 bytes of the response: for a
one-shot CLI, stderr is the only channel a refusal is reported on. A
stale runtime-root.txt from an earlier run in the same results directory
is removed at start, so a failed run leaves no handoff at all.
The contract test's fixture answers `cli index_repository` with a
minimal envelope when CBM_BENCH_PROBE_INDEX_OK is set, so the keep run
covers the successful-index handoff and asserts project.txt; the
unflagged run asserts a non-zero exit and the surfaced cause after a
refused index; a new keep run with a refused index asserts a non-zero
exit, the timing files, no surviving root and no handoff, a pre-seeded
stale one included. Against the previous head the test fails on
"benchmark-index exited 0 after a refused index".
Signed-off-by: Anton Standrik <astandrik@yandex-team.ru>
§7 step 8 removed the kept runtime root while the graph session of steps 4-7 may still have a daemon running under it; stop that daemon under the sourced CBM_RUNTIME_DIR first. §13 Reproducibility still cleared ~/.cache/codebase-memory-mcp/*.db and called the harness unflagged, which no longer describes what the harness does: it indexes into a run-private root, and only CBM_BENCH_KEEP_RUNTIME=1 hands that root, via runtime-root.txt, to the graph phase. Signed-off-by: Anton Standrik <astandrik@yandex-team.ru>
|
Thank you — reproduced exactly as you describe, and the retention contract now keys on the index, not on benchmark-index.sh (07ee617): the index call records the CLI's exit status; once every per-run file is Contract test: the fixture answers Docs (f913f0e): §7 step 8 stops the daemon under the sourced Verified with a real binary built from this head (Linux, gcc 13.3) and with the release binary on macOS: Not touched, as you noted: |
What does this PR do?
Part of #1696 (audit ledger), follow-up to #1691/#1695.
scripts/benchmark-index.shandscripts/benchmark-search-graph.shran the productwith no runtime or cache of their own: the benchmark repository was indexed into the
operator's live store, every one-shot joined the operator's account daemon, and the
timings depended on whatever that daemon was doing.
scripts/test-runtime.sh, callcbm_test_runtime_initandrun
cbm_test_runtime_cleanup "$BINARY"from the EXIT trap.benchmark-index.shstarts the private daemon before timing, soindex-time.txtmeasures the index alone, and recordssetup-time.txt(daemonactivation) and
total-time.txt(their sum — the figure comparable with earlierruns, which paid activation inside the index timing whenever no daemon was warm).
benchmark-search-graph.shnow takes<repo-path>instead of<project-name>:it indexes into the private cache (untimed) and times the queries against a daemon
it keeps warm, so no timing includes activation. No performance claim is made.
tests/test_benchmark_runtime_isolation_contract.sh: environment-probe fixturefor both scripts plus the presence of the three timing files, the keep handoff
(kept only after a successful index) and the refused-index contract (non-zero
exit, cause surfaced, no root, no handoff). Fails on
mainwithFAIL: benchmark-index exposed the caller CBM_RUNTIME_DIR to a product process; passeswith this change. Wired as Step 0i2 in
scripts/test.sh.docs/EVALUATION_PLAN.md§7 runsbenchmark-index.shand then answers graphquestions from its own MCP session, which assumed the index stayed in the live store.
The harness therefore has an explicit, opt-in handoff: with
CBM_BENCH_KEEP_RUNTIME=1a run whose index succeeded stops its daemon, leaves the run-private root in place and
records the paths that reach it in
<results>/<lang>/runtime-root.txt(sourceable);ownership of that root, including its removal, passes to the caller. A failed or
refused index exits non-zero after the timing files are written, keeps nothing (no
root, no handoff — a stale one from an earlier run in the same results directory
included) and names its cause under
--- index stderr ---. §7 sets the flag, sourcesthe file before the graph session, stops the daemon and removes the root in step 8;
§13 no longer clears
~/.cache/codebase-memory-mcp/*.db. Never on by default.Checklist
git commit -s)make -f Makefile.cbm test) — full C suite (ASan/UBSan,sequential) on Linux/gcc 13.3 at f913f0e: 8114 passed, 8 skipped, exit 0;
scripts/test.shdefault (143/143 suites) and
--tsan(1180 passed, 8 skipped) on the merge resultwith
maine783f73make -f Makefile.cbm lint-ci) — clang-format 20.1.8 + cppcheck2.20.0 (the CI toolchain) at f913f0e, clean