{"record":{"id":"21fe1d9438c8b390","repo":"can1357/oh-my-pi","slug":"cmux-browser-screenshot-response-did-not-include-p","errorCode":null,"errorMessage":"cmux browser screenshot response did not include png_base64","messagePattern":"cmux browser screenshot response did not include png_base64","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts","lineNumber":853,"sourceCode":"\t\tconst pollingMs = typeof opts?.polling === \"number\" ? opts.polling : 200;\n\t\tconst deadline = Date.now() + timeoutMs;\n\t\twhile (Date.now() <= deadline) {\n\t\t\tconst value = typeof fn === \"string\" ? await this.#evalScript<unknown>(fn) : await this.evaluate(fn, ...args);\n\t\t\tif (value) return value;\n\t\t\tawait untilAborted(signal, () => Bun.sleep(pollingMs));\n\t\t}\n\t\tthrow new ToolError(`page.waitForFunction() timed out after ${timeoutMs}ms`);\n\t}\n\n\tasync #evalScript<TResult>(script: string, timeoutMs?: number): Promise<TResult> {\n\t\tconst result = (await this.#request(\"browser.eval\", { script }, timeoutMs)) as CmuxEvalResult;\n\t\treturn result.value as TResult;\n\t}\n\n\tasync #captureScreenshotPng(timeoutMs: number): Promise<CmuxScreenshotResult & { png_base64: string }> {\n\t\tconst result = (await this.#request(\"browser.screenshot\", {}, timeoutMs)) as CmuxScreenshotResult;\n\t\tif (typeof result.png_base64 !== \"string\" || result.png_base64.length === 0) {\n\t\t\tthrow new ToolError(\"cmux browser screenshot response did not include png_base64\");\n\t\t}\n\t\treturn result as CmuxScreenshotResult & { png_base64: string };\n\t}\n\n\tasync #selectorAction<TResult = void>(\n\t\tselector: string,\n\t\taction: string,\n\t\targs: Record<string, unknown> = {},\n\t): Promise<TResult> {\n\t\tconst spec = this.#selectorSpec(selector);\n\t\tconst nativeSelector = this.#nativeSelector(spec);\n\t\tif (nativeSelector && action !== \"select\" && action !== \"uploadFile\") {\n\t\t\tswitch (action) {\n\t\t\t\tcase \"click\":\n\t\t\t\t\tawait this.#request(\"browser.click\", { selector: nativeSelector });\n\t\t\t\t\treturn undefined as TResult;\n\t\t\t\tcase \"dblclick\":\n\t\t\t\t\tawait this.#request(\"browser.dblclick\", { selector: nativeSelector });","sourceCodeStart":835,"sourceCodeEnd":871,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts#L835-L871","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: screenshot immediately after goto on a flaky surface\nawait tab.goto(url);\nawait tab.screenshot();\n// after: let the page settle and retry once\nawait tab.goto(url);\nawait tab.waitForNavigation().catch(() => {});\ntry {\n  await tab.screenshot();\n} catch {\n  await Bun.sleep(500);\n  await tab.screenshot();\n}","handlingStrategy":"retry","validationCode":"// guard the raw daemon result shape before using it\nconst result = (await request(\"browser.screenshot\", {})) as CmuxScreenshotResult;\nif (typeof result?.png_base64 !== \"string\" || result.png_base64.length === 0) {\n  throw new Error(\"daemon returned no png_base64 — check daemon version/logs\");\n}","typeGuard":"function hasPng(r: unknown): r is CmuxScreenshotResult & { png_base64: string } {\n  return typeof r === \"object\" && r !== null &&\n    typeof (r as CmuxScreenshotResult).png_base64 === \"string\" &&\n    (r as CmuxScreenshotResult).png_base64.length > 0;\n}","tryCatchPattern":"let shot: string;\ntry {\n  shot = await tab.screenshot();\n} catch (err) {\n  if (err instanceof ToolError && err.message.includes(\"png_base64\")) {\n    await Bun.sleep(500); // let the surface settle, then retry once\n    shot = await tab.screenshot();\n  } else throw err;\n}","preventionTips":["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"],"tags":["browser","screenshot","protocol","daemon"],"backgroundTag":"unexpected-response-shape","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}