Hmbown/CodeWhale · error · ExecError

ssh : timed out

Error message

ssh ${userHost}: timed out

What it means

Thrown by the remote transport's remote() when the ssh invocation to the user's host (running the remote node agent) exceeds its timeout (default 25s, overridable via opts.timeoutMs). The remote agent prints a JSON receipt over stdout; when ssh stalls instead of returning, the request is failed with this error rather than hanging.

Solutions

  1. Retry the request — transient ssh latency is the most common cause.
  2. Pass a larger timeoutMs in opts for legitimately slow remote operations.
  3. Check basic connectivity to userHost (`ssh <userHost> echo ok`) and fix network/VPN issues.
  4. Set up ssh ControlMaster/keepalives to keep a warm connection and reduce per-call setup latency.

Example fix

// before
await computer.remote({ tool: "screenshot", args: {} }); // times out at 25s
// after
await computer.remote({ tool: "screenshot", args: {} }, { timeoutMs: 60_000 });
Defensive patterns

Strategy: retry

Validate before calling

import { execFile } from "node:child_process";
import { promisify } from "node:util";
const pexec = promisify(execFile);
await pexec("ssh", [userHost, "echo", "ok"], { timeout: 5_000 }); // host must answer

Try / catch

try {
  const reply = await computer.remote(req);
} catch (e) {
  if (e instanceof Error && e.message.includes(": timed out")) {
    // retry with a larger timeoutMs; verify `ssh <userHost> echo ok` works
  }
}

Prevention

When it happens

Trigger: A remote computer request where `ssh [...userHost...] node <agent> ...` runs past timeoutMs — slow network, the remote host unreachable-but-not-refusing, or the in-container agent taking too long on a heavy tool call.

Common situations: Flaky VPN/Wi-Fi, sleeping remote machine waking up, DNS stalls, large tool operations on the remote desktop exceeding 25s, or ssh multiplexing/master connection degraded.

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/877451b817ef3c8a. Report an issue: GitHub.

Appendix: source

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

  const userHost = computer.user ? `${computer.user}@${computer.host}` : computer.host;
  const portArgs = computer.port ? ["-p", String(computer.port)] : [];
  const remoteAgent = safeRemotePath(computer.agentPath ?? ".codewhale-cu/agent/agent.mjs");
  const base = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=8", "-o", "StrictHostKeyChecking=accept-new", ...portArgs, userHost];
  const ex = {
    kind: "ssh",
    base,
    userHost,
    remoteAgent,
    run(cmd, args = [], opts = {}) {
      // Local side commands (e.g. ssh itself) run directly.
      return run(cmd, args, opts);
    },
    async remote(request, opts = {}) {
      const r = await run("ssh", [...base, "node", 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(`ssh ${userHost}: timed out`, r);
      if (r.code !== 0) throw new ExecError(`ssh ${userHost} exited ${r.code}: ${r.stderr.trim().slice(0, 400)}`, r);
      // The agent prints exactly one JSON line; anything before it is MOTD noise.
      const line = r.stdout.trim().split("\n").filter((l) => l.startsWith("{")).pop();
      const reply = line ? JSON.parse(line) : null;
      if (!reply) throw new ExecError(`ssh ${userHost}: agent returned no JSON receipt`, r);
      return reply;
    },
  };
  attachPersistentChannel(ex, binding, ["ssh", ...base, "node", remoteAgent, "--serve"]);
  return ex;
}

/**
 * docker executor: a spawned task-owned desktop container. Same agent contract
 * as ssh, but the channel is `docker exec` — no sshd, no keys, the container
 * boundary itself is the isolation. Every call goes through
 * docker/agent-exec.sh, which joins the desktop session env (display + bus)
 * the container entrypoint recorded before serving requests.

View on GitHub (pinned to 73e0f67d83)