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
- Read the embedded stderr and run `docker rm -f <container>` manually to see the full error.
- If the container is stuck: `docker kill <container>` then `docker rm -f <container>`.
- Restart the Docker daemon if it became unresponsive, then retry destroy.
- 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
- Keep the daemon healthy across spawn/destroy (avoid daemon restarts mid-session).
- Don't switch DOCKER_HOST/context between spawn and destroy.
- If containers get stuck, kill before force-removing.
- Run rootless docker with consistent user/permissions.
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
- cloud agent was created but its labels could not be applied…
- docker exec : agent returned no JSON receipt
- docker exec exited
- docker exec : timed out
- docker_unavailable
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)