NousResearch/hermes-agent

Docker container cleanup on run failure

Cluster 7439 · environments. Data as of

Nothing open on GitHub. Fix merged in #39412. Check it covers your case.

The short answer

0open issues
0open PRs
5items grouped
Known?
Yes. 5 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #39412. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #39412. That is separate from the fix event.
Still open
Nothing open on GitHub.

This cluster addresses a bug where Docker containers are left in a 'Created' (orphaned) state when 'docker run' or session initialization fails. Multiple fix attempts and follow-up PRs propose cleanup logic to handle these leaked containers.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #39412 fix(docker): clean up orphaned container when docker run fails (salvage #7440)

    merged to main

    files: tests/tools/test_docker_environment.py, tools/environments/docker.py

    Show the diff
    • tests/tools/test_docker_environment.py
      @@ -786,6 +786,84 @@ def test_reuse_falls_back_to_fresh_run_when_start_fails(monkeypatch):
           assert run_invocations, "fallback to fresh docker run must happen on start failure"
       
       
      +def test_failed_docker_run_cleans_up_orphaned_container(monkeypatch):
      +    """When ``docker run`` fails (e.g. exit 125), the partially-created
      +    container must be removed by name.
      +
      +    Docker can create the container object before failing to start it,
      +    leaving a stale ``Created`` container. The exited-only orphan reaper
      +    (``reap_orphan_containers``, ``status=exited``) never catches a
      +    ``Created`` orphan, so without this cleanup it leaks permanently.
      +    Regression for #7439. Salvage of #7440 (@Tranquil-Flow).
      +    """
      +    monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
      +    monkeypatch.setattr(docker_env, "_get_active_profile_name", lambda: "default")
      +
      +    cleanup_calls = []
      +
      +    def _run(cmd, **kwargs):
      +        if isinstance(cmd, list) and len(cmd) >= 2:
      +            sub = cmd[1]
      +            if sub == "version":
      +                return subprocess.CompletedProcess(cmd, 0, stdout="Docker version", stderr="")
      +            if sub == "ps":
      +                # No reusable container -> fall through to a fresh `docker run`.
      +                return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
      +            if sub == "run":
      +                raise subprocess.CalledProcessError(
      +                    125, cmd, output="", stderr="docker: Error response from daemon"
      +                )
      +            if sub == "rm":
      +                cleanup_calls.append(list(cmd))
      +                return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
      +        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
      +
      +    monkeypatch.setattr(docker_env.subprocess, "run", _run)
      +
      +    with pytest.raises(subprocess.CalledProcessError):
      +        _make_dummy_env()
      +
      +    assert len(cleanup_calls) == 1, "docker rm should be called once for the orphaned container"
      +    rm_cmd = cleanup_calls[0]
      +    assert rm_cmd[1] == "rm" and rm_cmd[2] == "-f"
      +    assert rm_cmd[3].startswith("hermes-"), "should remove the container by its generated name"
      +
      +
      +def test_docker_run_timeout_cleans_up_orphaned_container(monkeypatch):
      +    """When ``docker run`` times out (e.g. slow image pull), the
      +    partially-created container must be removed. Salvage of #7440
      +    (@Tranquil-Flow); regression for #7439.
      +    """
      +    monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
      +    monkeypatch.setattr(docker_env, "_get_active_profile_name", lambda: "default")
      +
      +    cleanup_calls = []
      +
      +    def _run(cmd, **kwargs):
      +        if isinstance(cmd, list) and len(cmd) >= 2:
      +            sub = cmd[1]
      +            if sub == "version":
      +                return subprocess.CompletedProcess(cmd, 0, stdout="Docker version", stderr="")
      +            if sub == "ps":
      +                return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
      +            if sub == "run":
      +                raise subprocess.TimeoutExpired(cmd, 120)
      +            if sub == "rm":
      +                cleanup_calls.append(list(cmd))
      +                return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
      +        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
      +
      +    monkeypatch.setattr(docker_env.subprocess, "run", _run)
      +
      +    with pytest.raises(subprocess.TimeoutExpired):
      +        _make_dummy_env()
      +
      +    assert len(cleanup_calls) == 1, "docker rm should be called once for the orphaned container"
      +    rm_cmd = cleanup_calls[0]
      +    assert rm_cmd[1] == "rm" and rm_cmd[2] == "-f"
      +    assert rm_cmd[3].startswith("hermes-"), "should remove the container by its generated name"
      +
      +
       def test_no_reuse_when_persist_across_processes_disabled(monkeypatch):
           """Opt-out path: ``persist_across_processes=False`` skips the ps probe
           entirely and always starts a fresh container, matching the pre-fix
    • tools/environments/docker.py
      @@ -854,13 +854,31 @@ def __init__(
                       "sleep", "infinity",  # no fixed lifetime — idle reaper handles cleanup
                   ]
                   logger.debug(f"Starting container: {' '.join(run_cmd)}")
      -            result = subprocess.run(
      -                run_cmd,
      -                capture_output=True,
      -                text=True,
      -                timeout=120,  # image pull may take a while
      -                check=True,
      -            )
      +            try:
      +                result = subprocess.run(
      +                    run_cmd,
      +                    capture_output=True,
      +                    text=True,
      +                    timeout=120,  # image pull may take a while
      +                    check=True,
      +                )
      +            except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
      +                # Docker may create the container object before `docker run`
      +                # fails to start it (e.g. exit code 125 when the daemon isn't
      +                # ready, or a timeout mid-pull). That orphan is left in
      +                # "Created" state — which the exited-only orphan reaper
      +                # (reap_orphan_containers, status=exited) never catches, so it
      +                # leaks permanently. Remove it by its known name before
      +                # re-raising. See #7439.
      +                logger.warning(
      +                    "docker run failed for %s, cleaning up orphaned container: %s",
      +                    container_name, e,
      +                )
      +                subprocess.run(
      +                    [self._docker_exe, "rm", "-f", container_name],
      +                    capture_output=True, timeout=10,
      +                )
      +                raise
                   self._container_id = result.stdout.strip()
                   logger.info(f"Started container {container_name} ({self._container_id[:12]})")
       

3 PRs closed without merging

From the close comments: closed without comment 2 · other 1

  • #7440 fix(docker): clean up orphaned container when docker run fails · other → #7498, #7439
  • #7498 fix(docker): clean up containers when docker run or init_session fails · closed without comment
  • #11535 fix(docker): clean up orphaned containers when docker run fails · closed without comment
Closed issues · 1
  • Closed#7439Docker containers leak in Created state when docker run fails
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.40: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
environments
Merged PRs
#39412
Closed issues
1
Attempts
3 PRs closed without merging
Fix link checked
5 Jun 2026

For your agent

The same facts, typed.