Hmbown/CodeWhale · error · SpawnError

cleanup_failed

cleanup_failed

Error message

docker rm -f ${container} failed: ${trim(r.stderr)}

What it means

Thrown by destroyDockerComputer() after the container passed the spawn-label check but `docker rm -f <container>` exits non-zero. The docker CLI's stderr is embedded in the message. The container passed both name validation and the SPAWN_LABEL ownership check, so this is a genuine removal failure.

Solutions

  1. Read the embedded stderr and run `docker rm -f <container>` manually to see the full error.
  2. If the container is stuck: `docker kill <container>` then `docker rm -f <container>`.
  3. Restart the Docker daemon if it became unresponsive, then retry destroy.
  4. Verify no DOCKER_HOST/context switch occurred between spawn and destroy.

Example fix

// before
// Error: docker rm -f cu-spawn-x-abc failed: ... is not running
// after
$ docker kill cu-spawn-x-abc && docker rm -f cu-spawn-x-abc
// then remove the stale registry entry
Defensive patterns

Strategy: try-catch

Validate before calling

import { execFile } from "node:child_process";
import { promisify } from "node:util";
const pexec = promisify(execFile);
const { stdout } = await pexec("docker", ["container", "inspect", "--format", "{{.State.Status}}", container]);
// if stdout is "paused" or "restarting", fix state before destroy

Try / catch

try {
  await destroyDockerComputer(computer);
} catch (e) {
  if (e?.code === "cleanup_failed") {
    // run `docker kill` then `docker rm -f` manually; then drop the registry entry
  }
}

Prevention

When it happens

Trigger: destroyDockerComputer() where the container exists, its spawn label is "1", but `docker rm -f` fails — e.g. the container is paused/unremovable, the daemon died mid-call, or permissions changed between inspect and rm.

Common situations: Docker daemon restarting under load, container in a stuck state (`docker kill` first), rootless-docker permission quirks, or DOCKER_HOST/context switched between calls.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/0d8876cc1f7b4f2c. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/spawn.mjs:132

    throw err;
  }
}

/**
 * Destroy a spawned container — but only one this plugin created. A docker
 * computer whose container lacks our spawn label is left running and reported
 * not_spawned; removing its registry entry is still the caller's choice.
 */
export async function destroyDockerComputer(computer) {
  const container = computer?.container;
  if (!container || !CONTAINER_RE.test(container)) {
    throw new SpawnError("invalid_container", "docker computer has no valid container name");
  }
  const insp = await docker(["container", "inspect", "--format", `{{index .Config.Labels "${SPAWN_LABEL}"}}`, container], { timeoutMs: 10_000, signal: null });
  if (insp.code !== 0) return { destroyed: false, reason: "container_gone" };
  if (insp.stdout.trim() !== "1") return { destroyed: false, reason: "not_spawned" };
  const r = await docker(["rm", "-f", container], { timeoutMs: 20_000, signal: null });
  if (r.code !== 0) throw new SpawnError("cleanup_failed", `docker rm -f ${container} failed: ${trim(r.stderr)}`, r);
  return { destroyed: true };
}

/**
 * Session teardown: destroy every spawned container this MCP process owns.
 * Other sessions' spawns are left alone — the registry is shared but a
 * container belongs to the process that created it.
 */
export async function destroySessionSpawns() {
  const listed = await docker(["ps", "-aq", "--filter", `label=${SPAWN_LABEL}=1`, "--filter", `label=${SESSION_LABEL}=${SESSION_ID}`], { timeoutMs: 10_000, signal: null });
  if (listed.code !== 0) return { destroyed: [], error: trim(listed.stderr) };
  const ids = listed.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
  const destroyed = [];
  for (const container of ids) {
    const r = await docker(["rm", "-f", container], { timeoutMs: 15_000, signal: null });
    destroyed.push({ container, ok: r.code === 0 });
  }
  return { destroyed };

View on GitHub (pinned to 73e0f67d83)