can1357/oh-my-pi · error · ToolError
cmux browser screenshot response did not include png_base64
Error message
cmux browser screenshot response did not include png_base64
What it means
CmuxTab's internal #captureScreenshotPng() calls the cmux daemon's browser.screenshot method and expects a CmuxScreenshotResult containing a non-empty png_base64 field. This ToolError is thrown when the response lacks the field or contains an empty string — i.e. the daemon acknowledged the request but returned no image payload, indicating a protocol/daemon version mismatch or a capture failure on the daemon side.
Source
Thrown at packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts:853
const pollingMs = typeof opts?.polling === "number" ? opts.polling : 200;
const deadline = Date.now() + timeoutMs;
while (Date.now() <= deadline) {
const value = typeof fn === "string" ? await this.#evalScript<unknown>(fn) : await this.evaluate(fn, ...args);
if (value) return value;
await untilAborted(signal, () => Bun.sleep(pollingMs));
}
throw new ToolError(`page.waitForFunction() timed out after ${timeoutMs}ms`);
}
async #evalScript<TResult>(script: string, timeoutMs?: number): Promise<TResult> {
const result = (await this.#request("browser.eval", { script }, timeoutMs)) as CmuxEvalResult;
return result.value as TResult;
}
async #captureScreenshotPng(timeoutMs: number): Promise<CmuxScreenshotResult & { png_base64: string }> {
const result = (await this.#request("browser.screenshot", {}, timeoutMs)) as CmuxScreenshotResult;
if (typeof result.png_base64 !== "string" || result.png_base64.length === 0) {
throw new ToolError("cmux browser screenshot response did not include png_base64");
}
return result as CmuxScreenshotResult & { png_base64: string };
}
async #selectorAction<TResult = void>(
selector: string,
action: string,
args: Record<string, unknown> = {},
): Promise<TResult> {
const spec = this.#selectorSpec(selector);
const nativeSelector = this.#nativeSelector(spec);
if (nativeSelector && action !== "select" && action !== "uploadFile") {
switch (action) {
case "click":
await this.#request("browser.click", { selector: nativeSelector });
return undefined as TResult;
case "dblclick":
await this.#request("browser.dblclick", { selector: nativeSelector });View on GitHub (pinned to 9690622007)
Solutions
- Update the cmux daemon (and CLI) so both sides agree on the browser.screenshot response schema.
- Retry the screenshot after ensuring the page is idle (waitForNavigation/waitForSelector) — transient capture races often succeed on retry.
- Check daemon logs for an underlying capture failure (crashed renderer, no active tab).
- Verify no intermediary (proxy, size limit) is truncating the base64 response; test with a simple about:blank tab.
Example fix
// before: screenshot immediately after goto on a flaky surface
await tab.goto(url);
await tab.screenshot();
// after: let the page settle and retry once
await tab.goto(url);
await tab.waitForNavigation().catch(() => {});
try {
await tab.screenshot();
} catch {
await Bun.sleep(500);
await tab.screenshot();
} Defensive patterns
Strategy: retry
Validate before calling
// guard the raw daemon result shape before using it
const result = (await request("browser.screenshot", {})) as CmuxScreenshotResult;
if (typeof result?.png_base64 !== "string" || result.png_base64.length === 0) {
throw new Error("daemon returned no png_base64 — check daemon version/logs");
} Type guard
function hasPng(r: unknown): r is CmuxScreenshotResult & { png_base64: string } {
return typeof r === "object" && r !== null &&
typeof (r as CmuxScreenshotResult).png_base64 === "string" &&
(r as CmuxScreenshotResult).png_base64.length > 0;
} Try / catch
let shot: string;
try {
shot = await tab.screenshot();
} catch (err) {
if (err instanceof ToolError && err.message.includes("png_base64")) {
await Bun.sleep(500); // let the surface settle, then retry once
shot = await tab.screenshot();
} else throw err;
} Prevention
- Keep the cmux daemon and CLI versions in sync
- Let navigation/rendering settle before capturing screenshots
- Watch daemon logs for renderer crashes that yield empty captures
- Avoid capturing while the tab is mid-navigation or closing
When it happens
Trigger: Calling tab.screenshot() against a cmux daemon that returns a different response shape (older/newer daemon without png_base64); the daemon failed to capture (headless surface not ready, page crashed) but still returned a 200-style result; a race where the tab closed mid-capture producing an empty payload.
Common situations: Version skew between the CLI's CmuxTab client and the installed cmux daemon; daemon-side renderer crash after heavy pages; screenshots taken during navigation when the surface has no frame to grab; proxy/middleware stripping or truncating the large base64 response.
Related errors
- response.ok must be a boolean
- operation.for must be ready or exit
- Unknown daemon operation: ${op}
- result.daemons must be an array
- Connected to ${cdpUrl} but puppeteer.connect failed: ${(err
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/21fe1d9438c8b390.
Report an issue: GitHub.