Hmbown/CodeWhale · error · ExecError

docker exec : agent returned no JSON receipt

Error message

docker exec ${container}: agent returned no JSON receipt

What it means

After a successful (exit 0) `docker exec` run, the transport looks for the last stdout line starting with '{' as the agent's JSON reply and throws this ExecError if none exists. The agent must print exactly one JSON receipt line; this error means it printed none.

Solutions

  1. docker exec <container> /bin/sh <remoteAgent> with sample args to inspect raw output
  2. Reinstall the agent into the container (re-run the docker agent install path)
  3. Verify the agent file inside the container is non-empty and is the real agent.mjs
  4. Check node availability/version in the container
  5. Confirm nothing in the container's shell setup discards stdout

Example fix

// before
await ex.remote({ tool: "platform" }); // no JSON receipt
// after
const chk = await run("docker", ["exec", container, "/bin/sh", "-c", `wc -c ${remoteAgent}`]);
if (chk.code !== 0 || chk.stdout.trim().startsWith("0")) await installDockerAgent(ex, computer);
await ex.remote({ tool: "platform" });
Defensive patterns

Strategy: try-catch

Validate before calling

const chk = await run("docker", ["exec", container, "/bin/sh", "-c", `test -s ${remoteAgent} && echo ok`]);
if (chk.code !== 0 || !chk.stdout.includes("ok")) await installDockerAgent(computer);

Type guard

function isNoReceiptError(e) { return e instanceof ExecError && e.message.includes("no JSON receipt"); }

Try / catch

try {
  return await ex.remote(request, opts);
} catch (e) {
  if (e.message.includes("no JSON receipt")) {
    await installDockerAgent(computer); // reinstall, retry once
    return ex.remote(request, opts);
  }
  throw e;
}

Prevention

When it happens

Trigger: executor.remote() on docker transport where the exec succeeds but stdout contains no '{'-prefixed line: agent missing/empty in the container, agent crashed after printing nothing, wrong file at the agent path, or stdout hijacked by wrapper scripts.

Common situations: Agent volume/upload missing in the container; entrypoint or shell rc echoes text but agent silently fails; stale container built without the agent; agent syntax unsupported by the container's node version.

Related errors


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

Appendix: source

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

  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);
    for (const f of fs.readdirSync(full)) {
      if (f.endsWith(".mjs") || f.endsWith(".m") || f.endsWith(".h")) rels.push(`src/${dir ? dir + "/" : ""}${f}`);
    }
  }

View on GitHub (pinned to 73e0f67d83)