Hmbown/CodeWhale · error · ExecError

scp failed

Error message

scp ${rel} failed: ${r.stderr.trim().slice(0, 300)}

What it means

During installRemoteAgent(), each agent file is uploaded with scp; when any scp invocation exits nonzero this ExecError is thrown with the relative file name and trimmed stderr (max 300 chars). It identifies exactly which agent file failed to copy and why.

Solutions

  1. Check the local file exists: the error's rel names it; rebuild the plugin srcDir if missing
  2. Verify the remote directory still exists (.codewhale-cu/agent) after mkdir
  3. Ensure scp is installed locally and the port flag (-P) matches the computer's port
  4. Retry installRemoteAgent() — transient network drops are common
  5. Read embedded stderr for cause (permission denied vs no such file)

Example fix

// before
await installRemoteAgent(ex, computer); // scp backends/screen.mjs failed: no such file
// after
for (const rel of rels) {
  const p = rel === "agent.mjs" ? path.join(PLUGIN_ROOT, "agent.mjs") : path.join(srcDir, rel.slice(4));
  if (!fs.existsSync(p)) throw new Error(`missing local agent file: ${p}`); // detect before scp
}
await installRemoteAgent(ex, computer);
Defensive patterns

Strategy: retry

Validate before calling

// ensure every local file to upload exists before starting scp loop
for (const rel of rels) {
  const p = rel === "agent.mjs" ? path.join(PLUGIN_ROOT, "agent.mjs") : path.join(srcDir, rel.slice(4));
  if (!fs.existsSync(p)) throw new Error(`missing local file for ${rel}: ${p}`);
}

Type guard

function isScpFailure(e) { return e instanceof ExecError && e.message.startsWith("scp "); }

Try / catch

try {
  await installRemoteAgent(ex, computer);
} catch (e) {
  if (e.message.startsWith("scp ")) {
    // transient network drop is common — retry with backoff
    return retryWithBackoff(() => installRemoteAgent(ex, computer), 3);
  }
  throw e;
}

Prevention

When it happens

Trigger: installRemoteAgent() loop hits an scp failure: local file missing (srcDir not built), remote dir removed between mkdir and scp, permission denied, scp binary absent, or connection drops mid-transfer.

Common situations: Plugin build output (srcDir) stale or deleted so localPath doesn't exist; remote .codewhale-cu wiped by another process; scp not installed on the local machine; custom ssh port mismatched (port not passed); network interruption on large transfers.

Related errors


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

Appendix: source

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

/** 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);
    for (const f of fs.readdirSync(full)) {
      if (f.endsWith(".mjs") || f.endsWith(".m") || f.endsWith(".h")) rels.push(`src/${dir ? dir + "/" : ""}${f}`);
    }
  }
  const marker = ".codewhale-cu/agent";
  let r = await run("ssh", [...ex.base, "mkdir", "-p", `${marker}/src/backends`], { timeoutMs: 15_000 });
  if (r.code !== 0) throw new ExecError(`ssh ${ex.userHost}: mkdir failed: ${r.stderr.trim().slice(0, 300)}`, r);
  for (const rel of rels) {
    const localPath = rel === "agent.mjs" ? path.join(PLUGIN_ROOT, "agent.mjs") : path.join(srcDir, rel.slice(4));
    const dest = safeRemotePath(`${marker}/${rel}`);
    r = await run("scp", [...(computer.port ? ["-P", String(computer.port)] : []), localPath, `${ex.userHost}:${dest}`], { timeoutMs: 30_000 });
    if (r.code !== 0) throw new ExecError(`scp ${rel} failed: ${r.stderr.trim().slice(0, 300)}`, r);
  }
  // Probe remote platform via the agent itself.
  const reply = await ex.remote({ tool: "platform" });
  return { installed: rels.length, remotePlatform: reply.platform, agentPath: `${marker}/agent.mjs` };
}

/** hdc (HarmonyOS) executor. Commands run on-device; files pull to local tmp. */
export function hdcExec(computer) {
  const targetArgs = computer.target ? ["-t", computer.target] : [];
  const shell = (args, opts = {}) => run("hdc", [...targetArgs, "shell", ...args], opts);
  return {
    kind: "hdc",
    targetArgs,
    run,
    runOk,
    shell,
    async pullFile(remotePath, localPath, opts = {}) {
      // HDC device captures use absolute paths; SSH agent paths are relative.

View on GitHub (pinned to 73e0f67d83)