Hmbown/CodeWhale · error · ExecError

screenshot did not return a valid raster

Error message

screenshot did not return a valid raster

What it means

After running the PowerShell capture, screenshot verifies the PNG file exists on disk and that the returned bounds metadata (x,y,w,h) are finite positive numbers. It throws this ExecError when the capture silently failed — file never written, empty/bad JSON from PowerShell, or a zero-sized bitmap — so callers never get a raster pointing at a missing or corrupt file.

Solutions

  1. Ensure an interactive desktop session is attached (keep the RDP session connected, or use console access)
  2. Check the embedded PowerShell stderr/log for Add-Type or GDI+ failures
  3. Retry the capture; transient session lock can cause it
  4. Catch ExecError and surface that screen capture is unavailable in this session type

Example fix

// before
const shot = await screenshot(); // throws in disconnected RDP
// after
try {
  const shot = await screenshot();
} catch (e) {
  if (String(e.message).includes("valid raster")) reattachConsoleSession();
}
Defensive patterns

Strategy: retry

Try / catch

async function screenshotWithRetry(backend, args = {}, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await backend.screenshot(args);
    } catch (e) {
      if (!String(e.message).includes("valid raster") || i === attempts - 1) throw e;
      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
    }
  }
}

Prevention

When it happens

Trigger: GDI CopyFromScreen failing (locked session, disconnected RDP session with no desktop, secure desktop/UAC prompt active); PowerShell crashing so meta is undefined; antivirus blocking the PNG write; timeout at 30s.

Common situations: Screenshots over RDP when the session is minimized/disconnected; headless Windows VMs without an interactive desktop; running as a Windows service (Session 0 isolation) with no visible desktop.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/win32.mjs:422

      if (Object.hasOwn(args, "app_ref") || Object.hasOwn(args, "window_id")) throw unsupportedSelector("Windows screenshot does not support app_ref or window_id; omit them for a desktop screenshot");
      const { display = activeDisplay, region, path: outPath } = args;
      if (display != null && (!Number.isInteger(display) || display < 1)) throw new ExecError("display index must be a positive integer");
      if (region != null && (!Array.isArray(region) || region.length !== 4 || !region.every(Number.isInteger) || region[2] <= 0 || region[3] <= 0)) throw new ExecError("region must be integer [x,y,width,height] with positive size");
      const dir = recordingsDir();
      fs.mkdirSync(dir, { recursive: true });
      const file = path.resolve(outPath || path.join(dir, `shot-${crypto.randomBytes(6).toString("hex")}.png`));
      const meta = await psJson(`Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing;
$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen;
${display == null ? "" : `$screens = [System.Windows.Forms.Screen]::AllScreens; if (${display} -gt $screens.Count) { throw 'display index is out of range' }; $bounds = $screens[${display - 1}].Bounds;`}
${region == null ? "" : `$crop = New-Object System.Drawing.Rectangle(${region.join(",")}); if (-not $bounds.Contains($crop)) { throw 'region is outside capture bounds' }; $bounds = $crop;`}
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height);
try {
  $g = [System.Drawing.Graphics]::FromImage($bmp);
  try { $g.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size); } finally { $g.Dispose(); }
  $bmp.Save('${file.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png);
} finally { $bmp.Dispose(); }
@{ x = $bounds.X; y = $bounds.Y; w = $bounds.Width; h = $bounds.Height } | ConvertTo-Json -Compress;`, { timeoutMs: 30_000 });
      if (!fs.existsSync(file) || ![meta.x, meta.y, meta.w, meta.h].every(Number.isFinite) || meta.w <= 0 || meta.h <= 0) throw new ExecError("screenshot did not return a valid raster");
      lastRaster = { file, bytes: fs.statSync(file).size, points: { x: meta.x, y: meta.y, w: meta.w, h: meta.h }, pixels: { w: meta.w, h: meta.h }, scale: 1, capturedAt: new Date().toISOString() };
      return { ...lastRaster };
    },
    zoom: async ({ source, region, path: outPath }) => {
      const src = source ?? lastRaster?.file;
      if (!src) throw new ExecError("no screenshot taken yet on this computer — call screenshot first");
      const out = outPath || path.join(recordingsDir(), `zoom-${crypto.randomBytes(4).toString("hex")}.png`);
      const script = `Add-Type -AssemblyName System.Drawing;
$img = [System.Drawing.Image]::FromFile('${src.replace(/'/g, "''")}');
$rect = New-Object System.Drawing.Rectangle(${Math.round(region[0])}, ${Math.round(region[1])}, ${Math.round(region[2])}, ${Math.round(region[3])});
$bmp = New-Object System.Drawing.Bitmap($rect.Width, $rect.Height);
$g = [System.Drawing.Graphics]::FromImage($bmp);
$g.DrawImage($img, (New-Object System.Drawing.Rectangle(0, 0, $rect.Width, $rect.Height)), $rect, [System.Drawing.GraphicsUnit]::Pixel);
$g.Dispose();
$bmp.Save('${out.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png);
$bmp.Dispose(); $img.Dispose();
Write-Output '{"ok": true}';`;
      const r = await psOk(script, { timeoutMs: 20_000 });

View on GitHub (pinned to 73e0f67d83)