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
- Retry the request — transient ssh latency is the most common cause.
- Pass a larger timeoutMs in opts for legitimately slow remote operations.
- Check basic connectivity to userHost (`ssh <userHost> echo ok`) and fix network/VPN issues.
- 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
- Configure ssh keepalives and ControlMaster for a warm connection.
- Set a generous timeoutMs for heavy remote tool operations.
- Keep the remote host awake (disable sleep or use wake-on-LAN).
- Monitor VPN/network stability before batch remote operations.
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.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- DS4 /v1/models timed out after 15 seconds at
- GET timeout
- Request timeout after 15 seconds
- spawn_failed
- SSE stream idle timeout after
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)