Skip to content

hardcoded /tmp paths prevent concurrent eval runs plus various m3 issues#121

Open
haroldship wants to merge 9 commits into
mainfrom
fix/115-tmp-file-collisions
Open

hardcoded /tmp paths prevent concurrent eval runs plus various m3 issues#121
haroldship wants to merge 9 commits into
mainfrom
fix/115-tmp-file-collisions

Conversation

@haroldship

@haroldship haroldship commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

If you run two M3 evals at the same time on one machine — or run compare.sh, which runs several one after another — they were all writing their logs and hand-off files to the same fixed spots in /tmp. Two runs writing to the same file at the same time step on each other: logs get mixed together, and a run's results bundle can end up staged with the wrong content.

While tracking that down, a handful of other bugs turned up along the way. Here's everything in this PR, in plain terms:

  1. The main bug ([Bug]: Hardcoded shared /tmp paths collide between concurrent benchmark runs (console log, summary, registry log) #115, commit e0993c2) — console logs, the FINAL SUMMARY hand-off file, and the registry log all lived at fixed /tmp paths shared by every run. Fix: each run now gets its own private folder (M3_RUN_TMP_DIR — a pre-set env var wins, otherwise a fresh mktemp dir is created), and eval.sh, compare.sh, run_groundedness_ab.sh, and run_with_container.sh all write there instead of the shared path. The bundle-builder (create_eval_bundle.py) checks the new per-run folder first and only falls back to the old shared path for logs left by older runs.
  2. Directory inputs crashed the bundler (commit 7186ba7)--m3-data can point at a folder instead of a single file, but the code that copies and hashes task data assumed a single file and crashed with IsADirectoryError. Fixed with shared helpers that handle folders too.
  3. Silent hangs left no trace (commit f66de56, diagnostics only, no behavior change) — when a run died with no one watching, the code couldn't tell a real Ctrl-C apart from an internal cancellation, and gave no clue where it got stuck. Added a way to timestamp real signals and periodically dump what every background task is doing, so a silent hang leaves a trail.
  4. Concurrent runs could clobber each other's results too, not just logs (commit 35b50ec) — prediction/ground-truth files were written to one shared results folder regardless of which experiment was running, and old runs' files were never cleaned up. Now each named experiment gets its own isolated, self-cleaning folder; old (un-named) runs keep the previous shared path.
  5. Domains after the first one silently lost all their tools (commit fe3545a) — a spot in the code was recomputing the full domain list for a task and overwriting the narrowed-down list you actually meant to run. So the first domain (e.g. "address") would complete fine, and every domain after it would fail with "Application 'airline' not found in registry" — because only the first domain's registry was ever started. Observed on cap3-guard-test.

Fixes #115.

Tests

uv run --no-sync pytest benchmarks/m3/tests/ benchmarks/helpers/tests/ -m sanity: 107 passed, 2 skipped (Vakra vendor not installed, pre-existing and unrelated), 0 failures.

Test plan for reviewers

  • Run uv run --no-sync pytest benchmarks/m3/tests/ benchmarks/helpers/tests/ -m sanity
  • Optional: run two concurrent ./benchmarks/m3/eval.sh --m3-data invocations on one host and confirm no artifact clobbering

Summary by CodeRabbit

  • New Features

    • Benchmark bundles now support task sources provided as complete directories, preserving their files and structure.
    • Benchmark runs use isolated temporary locations for logs, summaries, and generated outputs, improving reliability during concurrent runs.
    • Added clearer diagnostics for interrupted or stalled evaluations, including useful task activity information.
  • Bug Fixes

    • Prevented logs, summaries, predictions, and other artifacts from being overwritten or mixed between runs.
    • Improved handling of no-ground-truth evaluations and sequential domain processing.
    • Directory task content is now hashed consistently so changes are detected correctly.

Console log, FINAL SUMMARY hand-off file, and outer registry log all lived
at fixed /tmp paths, so two concurrent m3 runs on one host interleaved and
clobbered each other's artifacts (and bundles staged the mixed content).

eval.sh now resolves M3_RUN_TMP_DIR (pre-set env wins, else a private
mktemp dir per run) and keeps console/summary/registry logs inside it;
eval_m3.py reads M3_SUMMARY_FILE from the environment. compare.sh exports
one M3_RUN_TMP_DIR for all its sequential eval.sh runs and stages logs
from there, preserving the truncate-per-run semantics.
run_groundedness_ab.sh moves its arm marker into the same dir;
run_with_container.sh gains an M3_REGISTRY_LOG override.
create_eval_bundle.py checks the run-scoped dir first and keeps the
legacy fixed paths as fallback for logs left by older runs.

Other benchmarks share the same pattern (see #115) and are out of scope
here.
--m3-data accepts a directory of input/output files, but the bundle
task-copy/hash logic assumed a single file and crashed with
IsADirectoryError via shutil.copy2/open(). Add shared helpers that
copytree directories and hash their contents recursively.
run_config_mode's except (KeyboardInterrupt, asyncio.CancelledError)
branch has fired unattended across several bundles (2026-06-24,
2026-07-04, 2026-07-05, 2026-07-09), always seconds after a call_model
log line, with no human present. That branch alone can't tell a real
SIGINT apart from a bare CancelledError raised inside the process.

Add a SIGINT observer that timestamps real signal delivery without
changing Ctrl-C behavior (#91, #92), and a periodic stall watchdog
that dumps every live asyncio task's stack so a silent hang (no
timeout, no exception) leaves a trace of where execution is blocked.
… bundle

evaluate_single_task wrote _vakra/prediction/<domain>.json (and, in
GT mode, _vakra/groundtruth/) to the shared benchmarks/m3/results/
directory regardless of experiment. Two concurrent eval.sh runs for
different experiments hitting the same domain name could clobber each
other's prediction file there, and old runs' files were never cleared.

Use bundle_dir/results when a workspace bundle exists (same fallback
pattern _finalize_and_save_results already uses), so each named
experiment gets its own isolated, self-cleaning directory tree and the
files land inside the bundle automatically. Legacy (un-experimented)
runs keep the old shared path.
evaluate_single_task re-derived `domains` from
m3_data_loader.available_domains(task_id) and unconditionally
overwrote the caller's value with the full domain list for the task.
That discarded the single-domain narrowing rewrite_config_with_loader_
domains + expand_registry_config already apply upstream specifically
so the sequential per-service loop can start one registry per domain.

The first call (e.g. domain "address") would then try to walk every
domain for the task using only the registry actually started for the
first one, so every domain after it had zero tools registered.
Observed on cap3-guard-test: address completed fine, then airline
failed with "Application 'airline' not found in registry" / "Loaded 0
tools for 'airline'".
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@haroldship, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b67e882b-218e-4b80-9cb2-4e1b9da4394d

📥 Commits

Reviewing files that changed from the base of the PR and between d263c8a and f9ceb41.

📒 Files selected for processing (3)
  • benchmarks/helpers/bundle.py
  • benchmarks/m3/eval.sh
  • benchmarks/m3/tests/test_interrupt_diagnostics.py
📝 Walkthrough

Walkthrough

Bundle assembly now supports directory task sources with directory-aware hashes. M3 scripts use run-scoped artifact paths, evaluation outputs are bundle-scoped, and interrupt/stall diagnostics plus regression tests were added.

Changes

Directory task sources

Layer / File(s) Summary
Directory-aware bundle assembly
benchmarks/helpers/bundle.py, benchmarks/helpers/tests/test_bundle_workspace.py
Task copying and hashing now support files and recursive directories across single-run, comparison, and workspace bundles, with coverage for directory and file inputs.

M3 execution isolation and diagnostics

Layer / File(s) Summary
Run-scoped logs and temporary artifacts
benchmarks/m3/run_groundedness_ab.sh, benchmarks/m3/compare.sh, benchmarks/m3/eval.sh, scripts/create_eval_bundle.py
M3 logs, summaries, comparison markers, and bundle log fallbacks use shared run-scoped temporary paths while retaining legacy fallbacks.
Evaluation output and domain scoping
benchmarks/m3/eval_m3.py, benchmarks/m3/tests/test_sequential_domain_scoping.py, benchmarks/m3/tests/test_vakra_output_scoping.py
Evaluation preserves caller-provided domains and scopes Vakra prediction and scoring outputs under the bundle results directory.
Interrupt and stall diagnostics
benchmarks/m3/eval_m3.py, benchmarks/m3/tests/test_interrupt_diagnostics.py
SIGINT delivery is recorded, asyncio task stacks are periodically logged, watchdog teardown is managed, and diagnostic behavior is tested.
Container registry log configuration
benchmarks/m3/run_with_container.sh
Registry startup, failure diagnostics, and final log instructions use the configurable registry log path.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the PR and captures the main theme of fixing hardcoded /tmp paths and other M3 evaluation issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/115-tmp-file-collisions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

haroldship added a commit that referenced this pull request Jul 14, 2026
…ures (#122)

* fix(deps): bump pillow and json-repair to clear CI security-scan failures

CI's pip-audit step was failing (blocking PR #120 and #121) because two
dependencies had known security bugs with fixes already published.

- pillow 12.2.0 -> 12.3.0, json-repair 0.59.10 -> 0.61.4 (both just needed
  a lockfile bump, no code changes required)
- setuptools also has a known bug (fixed in 83.0.0), but torch pins it to
  "<82" so we can't move it ourselves; added it to the security recipe's
  ignore list with a comment, matching the existing docling/torch entries

* fix(ci): ignore setuptools CVE in the CI workflow's pip-audit too

CodeRabbit caught that the CI workflow runs its own copy of the
pip-audit command instead of `just security`, so the setuptools ignore
added in the justfile wasn't actually reaching CI — the Security check
was still red on this PR. Mirrored the same --ignore-vuln flag and
explanation into ci.yml.
@haroldship haroldship changed the title M3: scope per-run /tmp artifacts to a private directory, fix concurrent-run clobbering hardcoded /tmp paths prevent concurrent eval runs plus various m3 issues Jul 14, 2026
@haroldship haroldship marked this pull request as ready for review July 14, 2026 15:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
scripts/create_eval_bundle.py (1)

67-76: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Deduplicate log paths to prevent passing duplicate logs to the bundle.

If both run-scoped logs and legacy /tmp logs exist, this loop will append all of them to logs. This results in duplicate log file basenames being passed to the bundle creation process (e.g., two m3_registry.log files from different directories).

Consider stopping at the first found instance of each log type so that stale /tmp logs are ignored when the run-scoped logs are present.

♻️ Proposed refactor
-    fallbacks: list[Path] = []
     run_tmp_dir = os.getenv("M3_RUN_TMP_DIR")
-    if run_tmp_dir:
-        fallbacks.append(Path(run_tmp_dir) / "m3_registry.log")
-        fallbacks.append(Path(run_tmp_dir) / "m3_console.log")
-    fallbacks.append(Path("/tmp/m3_registry.log"))  # noqa: S108  # nosec B108
-    fallbacks.append(Path("/tmp/m3_console.log"))  # noqa: S108  # nosec B108
-    for fallback in fallbacks:
-        if fallback.is_file() and fallback.stat().st_size > 0:
-            logs.append(fallback)
+    for log_name in ("m3_registry.log", "m3_console.log"):
+        candidates = []
+        if run_tmp_dir:
+            candidates.append(Path(run_tmp_dir) / log_name)
+        candidates.append(Path("/tmp") / log_name)  # noqa: S108  # nosec B108
+        
+        for candidate in candidates:
+            if candidate.is_file() and candidate.stat().st_size > 0:
+                logs.append(candidate)
+                break
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/create_eval_bundle.py` around lines 67 - 76, Update the fallback log
collection loop in create_eval_bundle so it selects at most one existing path
for each log type, preferring the run-scoped M3_RUN_TMP_DIR files over legacy
/tmp files. Stop searching for a log type once its first preferred instance is
found, ensuring logs contains no duplicate m3_registry.log or m3_console.log
entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarks/helpers/bundle.py`:
- Around line 115-141: Update _task_source_hash to hash directory files
incrementally in chunks instead of calling p.read_bytes(), reusing the
chunked-reading approach already established by _file_sha256 while preserving
the relative-path ordering and hash output format.

In `@benchmarks/m3/eval.sh`:
- Around line 215-219: The cleanup and bundle argument construction in
create_bundle must tolerate execution before REGISTRY_LOG and CONSOLE_LOG are
initialized. At benchmarks/m3/eval.sh lines 215-219, collect only existing
registry and non-empty console logs before appending --log-file entries; at
lines 297-301, apply the same guarded collection and add --log-files only when
logs exist. Also guard the CONSOLE_LOG reference in the line 250 echo with a
default such as “<not set>”.

In `@benchmarks/m3/tests/test_interrupt_diagnostics.py`:
- Around line 42-57: Update
test_sigint_observer_timestamps_signal_and_preserves_default_behavior to
temporarily install signal.default_int_handler for SIGINT before calling
eval_m3._install_sigint_observer(), then restore the prior handler in cleanup.
Keep the existing KeyboardInterrupt and observer-warning assertions unchanged.

---

Nitpick comments:
In `@scripts/create_eval_bundle.py`:
- Around line 67-76: Update the fallback log collection loop in
create_eval_bundle so it selects at most one existing path for each log type,
preferring the run-scoped M3_RUN_TMP_DIR files over legacy /tmp files. Stop
searching for a log type once its first preferred instance is found, ensuring
logs contains no duplicate m3_registry.log or m3_console.log entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8bd6e4aa-d0f4-4039-84a3-2a2d508164e0

📥 Commits

Reviewing files that changed from the base of the PR and between 2602266 and d263c8a.

📒 Files selected for processing (11)
  • benchmarks/helpers/bundle.py
  • benchmarks/helpers/tests/test_bundle_workspace.py
  • benchmarks/m3/compare.sh
  • benchmarks/m3/eval.sh
  • benchmarks/m3/eval_m3.py
  • benchmarks/m3/run_groundedness_ab.sh
  • benchmarks/m3/run_with_container.sh
  • benchmarks/m3/tests/test_interrupt_diagnostics.py
  • benchmarks/m3/tests/test_sequential_domain_scoping.py
  • benchmarks/m3/tests/test_vakra_output_scoping.py
  • scripts/create_eval_bundle.py

Comment thread benchmarks/helpers/bundle.py
Comment thread benchmarks/m3/eval.sh
Comment thread benchmarks/m3/tests/test_interrupt_diagnostics.py
- guard unbound CONSOLE_LOG/REGISTRY_LOG in eval.sh's cleanup-trap path
- read directory task-source files in chunks in _task_source_hash
@haroldship

Copy link
Copy Markdown
Collaborator Author

Fixes Applied Successfully

Fixed 2 file(s) based on 2 CodeRabbit feedback item(s). (A third item, on test_interrupt_diagnostics.py, was reviewed and found not to apply to the current code — _install_sigint_observer() calls signal.default_int_handler directly rather than delegating to a previously-installed handler, so the suggested pytest-handler isolation isn't needed.)

Files modified:

  • benchmarks/m3/eval.sh
  • benchmarks/helpers/bundle.py

Commit: ef44c87

The latest autofix changes are on the fix/115-tmp-file-collisions branch.

_install_sigint_observer currently calls signal.default_int_handler
directly, so this isn't strictly needed today, but guards against a
future change to delegate to whatever handler was previously installed.
@haroldship

Copy link
Copy Markdown
Collaborator Author

Follow-up: also applied issue #3 defensively

On reflection, applied the third CodeRabbit finding too (test_interrupt_diagnostics.py), even though it doesn't affect current behavior — it isolates the SIGINT-observer test from pytest's own handler, guarding against a future change to _install_sigint_observer that delegates to the previously-installed handler instead of calling signal.default_int_handler directly.

File modified: benchmarks/m3/tests/test_interrupt_diagnostics.py

Commit: f9ceb41

All 3 review threads on this PR are now addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Hardcoded shared /tmp paths collide between concurrent benchmark runs (console log, summary, registry log)

1 participant