Skip to content

dead docker containers during an M3 run get silently miscounted as task failures#120

Open
haroldship wants to merge 14 commits into
mainfrom
worktree-m3-docker-env-health-check
Open

dead docker containers during an M3 run get silently miscounted as task failures#120
haroldship wants to merge 14 commits into
mainfrom
worktree-m3-docker-env-health-check

Conversation

@haroldship

@haroldship haroldship commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

M3 runs its tests against a bunch of docker containers that live outside this repo (one per "capability"). Sometimes one of those containers crashes or freezes partway through a run. Before this PR, M3 didn't notice — it just kept going, and every failure caused by the dead container got written down as if the agent itself had failed the task. That silently pollutes results with noise that's indistinguishable from a real bug after the fact.

This PR adds three tripwires that all lead to the same outcome when triggered: print a big, hard-to-miss banner, stop the run with exit code 3, and let --resume-experiment pick the run back up once the environment is fixed.

  1. Before starting each domain — quickly check the container is actually alive (docker inspect + a real docker exec) before wasting a restart on one that's already dead.
  2. While running — keep a rolling count of "this looks like the environment, not the agent" failures in a row, within a domain. If it crosses a threshold, stop immediately rather than waiting for the domain to finish — a single domain can have 100+ samples and run for hours.
  3. Look inside tool-call results, not just exceptions — some failures never raise a Python error at all. The connection just quietly reports something like "Connection closed" as if it were a normal reply, and the agent retries or shrugs and moves on. This teaches the detector to recognize that pattern too, for both single-turn and multi-turn shapes.
  4. At startup — if zero tools ever register within the existing 5-minute warmup window, that's the same class of problem at a different point in time. Now it aborts instead of silently continuing with a broken, empty registry.

This only covers the default sequential mode (batch_size < 2); batched/parallel mode is untouched.

Config knobs (env vars, all optional)

  • M3_ENV_HEALTH_CHECK (default true) — turn off the active per-domain check
  • M3_ENV_HEALTH_TIMEOUT (default 5) — seconds before an inspect/exec counts as timed out
  • M3_ENV_FAIL_STREAK (default 3) — consecutive environment-shaped failures before aborting

Bad values for the numeric knobs fall back to defaults with a warning instead of crashing.

Design spec: docs/superpowers/specs/2026-07-13-m3-docker-env-health-check-design.md
Implementation plan: docs/superpowers/plans/2026-07-13-m3-docker-env-health-check.md
Closes #119.

Tests

  • Automated: 84 tests in benchmarks/m3/tests/, all passing (2 pre-existing skips, unrelated — Vakra vendor not installed).
  • Manual, against a real docker container (capability_2_dashboard_apis), using real docker compose stop/down:
    • Killed the container mid-domain — the run aborted with the banner and exit code 3 after a few consecutive failures, instead of grinding through the rest of the domain.
    • Killed the container before the run started — the active health check caught it immediately, before the registry ever restarted for that domain.
    • Killed the container during warmup (before any app registered) — aborted with exit code 3 instead of the old "wait 5 minutes then proceed anyway" behavior.
    • In all three cases, the closing banner correctly surfaced the --resume-experiment <id> hint.

Test plan for reviewers

  • uv run --no-sync pytest benchmarks/m3/tests/ -m sanity
  • Optional: reproduce the manual scenarios above against a live M3 docker environment

Summary by CodeRabbit

  • New Features

    • Added Docker environment health checks for M3 evaluations.
    • Detects repeated environment-related failures and stops evaluation with a clear failure banner.
    • Reports dedicated environment-failure exit status and provides resume instructions when recovery bundles are available.
    • Aborts when registry startup does not complete within the warmup window.
  • Tests

    • Added comprehensive coverage for health detection, failure tracking, abort behavior, exit codes, and resume messaging.

Add _env_float() and _env_int() helpers that parse env vars with
graceful fallback to defaults + warning logs. Prevents ValueError
exceptions when M3_ENV_HEALTH_TIMEOUT or M3_ENV_FAIL_STREAK are
malformed (e.g., "three" or "abc" instead of numbers).
…ries

A single domain can hold 100+ samples and run for hours; detection
that only re-checks at domain boundaries (run_config_mode's sequential
loop) would grind through every remaining sample against a dead
container before anything noticed. Add a per-sample environment-
failure streak inside M3Evaluator.evaluate_all's sample loop, fresh
per domain, so it trips after a few consecutive failures instead of
waiting for the whole domain to finish.

Also fixes a second swallowing bug: evaluate_single_task's per-domain
except clause had no passthrough for EnvironmentFailureError, so even
a correctly-raised abort from inside the domain's evaluation would
have been silently logged and skipped to the next domain instead of
propagating to the existing abort path.
…st raised exceptions

A live run surfaced a failure mode neither detector caught: the registry
stays reachable (find_tools still works) but the MCP transport to the
dead container itself is closed. The tool call doesn't raise — it
returns "Connection closed" as its own value — the agent retries a
few times, then produces a normal-looking "I couldn't get the data"
final answer. result["error"] stays unset throughout, so both the
domain-level and sample-level streak checks (which only looked at
result["error"]) never saw it.

Add is_environment_shaped_result(), which checks both the top-level
error field and every tool call's own return value (multi-turn and
single-turn shapes), and wire EnvironmentFailureStreakTracker.record()
to use it instead of the narrower error-only check. Also add
"Connection closed" (and variants) to the marker list.
…of proceeding anyway

start_registry_server polled for up to 5 minutes waiting for the
registry to report at least one registered MCP application, but on
timeout it only logged a warning and proceeded — returning a "healthy"
registry handle with nothing behind it. That's the same failure class
this whole feature targets (a broken docker environment), just at
startup time rather than mid-run, and it was the one path our other
detectors couldn't see.

Zero apps registered after the full warmup window is unambiguous (the
loop's own success condition is len(apps) > 0, so "not all_ready"
after 300s means literally none ever did) — raise
EnvironmentFailureError instead. This reuses the function's existing
`except RuntimeError: raise` passthrough (added earlier for a stale-
registry mismatch) for free, since EnvironmentFailureError is a
RuntimeError subclass.
@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: 44 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: 6502de3d-fad5-46fd-a085-f879dcfdfe02

📥 Commits

Reviewing files that changed from the base of the PR and between ea332d1 and b8d774e.

📒 Files selected for processing (2)
  • benchmarks/m3/container_health.py
  • benchmarks/m3/tests/test_eval_m3_env_health_wiring.py
📝 Walkthrough

Walkthrough

M3 evaluation now probes Docker containers, detects consecutive environment-shaped failures, aborts broken runs with banners and resume hints, maps environment failures to exit code 3, and adds regression tests for detection, wiring, and shell reporting.

Changes

M3 environment health

Layer / File(s) Summary
Health detection and abort primitives
benchmarks/m3/container_health.py, benchmarks/m3/tests/test_container_health.py
Adds environment-shaped error/result classification, Docker inspect/exec probes, failure streak tracking, resume hints, banners, abort helpers, and unit coverage.
Evaluator health wiring
benchmarks/m3/eval_m3.py, benchmarks/m3/tests/test_eval_m3_env_health_wiring.py
Integrates health checks and streak tracking into sequential evaluation and registry warmup, preserves environment exceptions, parses health settings, and maps them to exit code 3.
Resume and exit reporting
benchmarks/m3/eval.sh, benchmarks/m3/tests/test_eval_sh_env_health.py
Adds exit-code-3 Docker failure reporting with optional --resume-experiment instructions and regression tests.

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

Sequence Diagram(s)

sequenceDiagram
  participant EvalShell
  participant M3Evaluation
  participant DockerContainer
  participant FailureTracker
  EvalShell->>M3Evaluation: start evaluation
  M3Evaluation->>DockerContainer: inspect and exec health probes
  DockerContainer-->>M3Evaluation: health result
  M3Evaluation->>FailureTracker: record task results
  FailureTracker-->>M3Evaluation: threshold status
  M3Evaluation-->>EvalShell: exit code 3 on environment failure
  EvalShell-->>EvalShell: print resume instructions
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Linked Issues check ✅ Passed The PR implements the requested health checks, streak detection, warning banner, and resume hint for M3 Docker helper failures.
Out of Scope Changes check ✅ Passed The added helpers, shell handling, and tests all support the M3 container-failure detection workflow and do not appear unrelated.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main problem this PR addresses: dead Docker containers being counted as task failures during M3 runs.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-m3-docker-env-health-check

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: detect a dead docker capability container mid-run, abort cleanly, resume dead docker containers during an M3 run get silently miscounted as task failures Jul 14, 2026
@haroldship
haroldship marked this pull request as ready for review July 14, 2026 13:59

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
benchmarks/m3/eval_m3.py (1)

1013-1123: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mirror the environment-failure streak check in the single-turn loop. Single-turn evaluation is still a live path, and this loop never calls self._env_fail_streak.record([result]), so a dead container can keep burning through the remaining tasks instead of aborting early.

🤖 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 `@benchmarks/m3/eval_m3.py` around lines 1013 - 1123, Add the same
environment-failure streak handling from the multiturn loop to the single-turn
loop after appending each result from evaluate_task. Call
self._env_fail_streak.record([result]), build the threshold/domain/identity
failure reason, print the failure banner with resume_hint_for(self.bundle_dir),
and raise EnvironmentFailureError when the threshold is reached.
🤖 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/m3/container_health.py`:
- Around line 90-129: Update check_container_health to catch OSError alongside
subprocess.TimeoutExpired around both subprocess.run calls for inspect and exec.
Return an unhealthy result with a descriptive environment/runtime failure reason
instead of allowing missing or broken container runtimes to propagate an
exception; preserve the existing timeout and successful health-check behavior.
- Around line 131-155: Validate threshold in
EnvironmentFailureStreakTracker.__init__ and reject any value less than or equal
to zero before storing it, so record() cannot immediately satisfy the streak
condition for invalid configuration. Preserve the existing positive-threshold
streak behavior and use the project’s established validation/error convention if
one is available.

In `@benchmarks/m3/tests/test_eval_m3_env_health_wiring.py`:
- Around line 70-78: Update
test_environment_failure_error_is_not_swallowed_by_the_generic_handler and the
related run_config_mode ordering test to locate run_config_mode’s own except
EnvironmentFailureError clause, searching after the first occurrence or within
the function’s extracted source. Compare that occurrence against
run_config_mode’s generic logger.error handler so the assertion fails if the
local passthrough clause is moved below the generic handler.

---

Outside diff comments:
In `@benchmarks/m3/eval_m3.py`:
- Around line 1013-1123: Add the same environment-failure streak handling from
the multiturn loop to the single-turn loop after appending each result from
evaluate_task. Call self._env_fail_streak.record([result]), build the
threshold/domain/identity failure reason, print the failure banner with
resume_hint_for(self.bundle_dir), and raise EnvironmentFailureError when the
threshold is reached.
🪄 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: 3d40deac-0395-44d1-bae4-8306792be008

📥 Commits

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

📒 Files selected for processing (6)
  • benchmarks/m3/container_health.py
  • benchmarks/m3/eval.sh
  • benchmarks/m3/eval_m3.py
  • benchmarks/m3/tests/test_container_health.py
  • benchmarks/m3/tests/test_eval_m3_env_health_wiring.py
  • benchmarks/m3/tests/test_eval_sh_env_health.py

Comment thread benchmarks/m3/container_health.py
Comment thread benchmarks/m3/container_health.py
Comment thread benchmarks/m3/tests/test_eval_m3_env_health_wiring.py
- catch OSError (missing/broken container runtime) in check_container_health
- validate EnvironmentFailureStreakTracker threshold >= 1
- fix except-clause ordering test to anchor on run_config_mode's own clause
@haroldship

Copy link
Copy Markdown
Collaborator Author

Fixes Applied Successfully

Fixed 2 file(s) based on 3 CodeRabbit feedback item(s).

Files modified:

  • benchmarks/m3/container_health.py
  • benchmarks/m3/tests/test_eval_m3_env_health_wiring.py

Commit: b8d774e

The latest autofix changes are on the worktree-m3-docker-env-health-check branch.

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.

[Feature]: Notice when the M3 docker "helper boxes" die mid-test, instead of quietly writing down wrong answers

1 participant