Hmbown/CodeWhale · error · ExecError

docker exec : timed out

Error message

docker exec ${container}: timed out

What it means

The Docker transport runs the remote agent via `docker exec` with a timeout (default 25s). When the run exceeds that limit it throws this ExecError signalling the docker exec timed out. It exists so callers can distinguish slow-hanging container commands from hard failures.

Solutions

  1. Raise the timeout: pass { timeoutMs: 60000 } (or higher) in opts to remote()
  2. Check container health: docker ps, docker logs <container>, docker stats
  3. Retry the request — a transient stall often resolves
  4. Ensure the container is running and not paused: docker unpause <container>
  5. Investigate the agent-side operation (e.g. screenshot) that may block indefinitely

Example fix

// before
await ex.remote({ tool: "screenshot" }); // default 25s too short
// after
await ex.remote({ tool: "screenshot" }, { timeoutMs: 120_000 });
Defensive patterns

Strategy: retry

Validate before calling

// check container is running and responsive before the call
const st = await run("docker", ["inspect", "-f", "{{.State.Running}}", container]);
if (st.code !== 0 || st.stdout.trim() !== "true") throw new Error(`container ${container} not running`);

Type guard

function isDockerTimeout(e) { return e instanceof ExecError && e.message.includes(": timed out"); }

Try / catch

try {
  return await ex.remote(request, opts);
} catch (e) {
  if (e.message.includes("timed out") && attempts < 3) return retryWithBackoff(attempts + 1, { ...opts, timeoutMs: (opts.timeoutMs ?? 25000) * 2 });
  throw e;
}

Prevention

When it happens

Trigger: executor.remote() on a docker transport where the `docker exec` invocation does not complete within opts.timeoutMs (default 25_000 ms): container paused, overloaded host, agent blocking on I/O, or docker daemon unresponsive.

Common situations: Container under heavy load or OOM-swapping; docker daemon stalled; agent waiting on a GUI/screenshot operation that never returns; host disk contention; running on a very slow remote machine.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/transport.mjs:258

 * the container entrypoint recorded before serving requests.
 */
export function dockerExec(computer, binding) {
  const container = safeRemotePath(computer.container);
  const remoteAgent = "/app/docker/agent-exec.sh";
  const ex = {
    kind: "docker",
    container,
    remoteAgent,
    run(cmd, args = [], opts = {}) {
      // Local side commands (docker itself) run directly.
      return run(cmd, args, opts);
    },
    async remote(request, opts = {}) {
      const r = await run("docker", ["exec", container, "/bin/sh", remoteAgent, b64({ args: request.args ?? {}, tool: request.tool, nonce: crypto.randomBytes(6).toString("hex") })], {
        timeoutMs: opts.timeoutMs ?? 25_000,
      });
      if (r.aborted) throw Object.assign(new ExecError("computer request cancelled", r), { code: "cancelled" });
      if (r.timedOut) throw new ExecError(`docker exec ${container}: timed out`, r);
      if (r.code !== 0) throw new ExecError(`docker exec ${container} exited ${r.code}: ${r.stderr.trim().slice(0, 400)}`, r);
      const line = r.stdout.trim().split("\n").filter((l) => l.startsWith("{")).pop();
      const reply = line ? JSON.parse(line) : null;
      if (!reply) throw new ExecError(`docker exec ${container}: agent returned no JSON receipt`, r);
      return reply;
    },
  };
  attachPersistentChannel(ex, binding, ["docker", "exec", "-i", container, "/bin/sh", remoteAgent, "--serve"]);
  return ex;
}

/** Push the self-contained remote agent + src tree to an ssh computer. */
export async function installRemoteAgent(computer) {
  const ex = sshExec(computer);
  const srcDir = path.join(PLUGIN_ROOT, "src");
  const rels = ["agent.mjs"];
  for (const dir of ["", "backends"]) {
    const full = path.join(srcDir, dir);

View on GitHub (pinned to 73e0f67d83)