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

  1. Update the cmux daemon (and CLI) so both sides agree on the browser.screenshot response schema.
  2. Retry the screenshot after ensuring the page is idle (waitForNavigation/waitForSelector) — transient capture races often succeed on retry.
  3. Check daemon logs for an underlying capture failure (crashed renderer, no active tab).
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/21fe1d9438c8b390. Report an issue: GitHub.