Hmbown/CodeWhale · error · ExecError

malformed JPEG at byte

Error message

malformed JPEG at byte ${at}: ${file}

What it means

While walking JPEG segment markers to find the SOFn frame header (which carries width/height), `imagePixels` expects every segment to begin with the 0xFF marker byte. If the byte at the current offset is not 0xFF, the file is not a well-formed JPEG and `malformed JPEG at byte <at>: <file>` is thrown.

Solutions

  1. Re-capture or re-copy the screenshot; the file is corrupt.
  2. Wait for the capture process to finish writing before reading dimensions.
  3. Verify the JPEG with a decoder (e.g. `sips -g pixelWidth`) to confirm corruption.
  4. Add a readiness check (file size stable across two stats) before reading.

Example fix

// before\nimagePixels(shotPath); // file may still be flushing\n// after\nawait waitForFileSettled(shotPath);\nimagePixels(shotPath);
Defensive patterns

Strategy: fallback

Validate before calling

// confirm the file is settled before parsing\nconst s1 = fs.statSync(file).size;\nawait sleep(150);\nif (fs.statSync(file).size !== s1) throw new Error("file still being written");

Try / catch

try { return imagePixels(file); } catch (e) { if (/malformed JPEG/.test(e.message)) { await recaptureScreenshot(file); return imagePixels(file); } throw e; }

Prevention

When it happens

Trigger: A file that starts with the JPEG SOI magic (0xFF 0xD8) but is corrupted or truncated afterwards — mid-stream corruption, a partially written screenshot, or appended/truncated data shifting segment lengths.

Common situations: Disk-full during capture leaving a truncated JPEG, a file still being written by the capture process when dimensions are read, or bit-flips from a bad transfer.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:49

 * Pixel dimensions from a PNG IHDR or a JPEG frame header, reading only the
 * bytes that carry them rather than the whole raster.
 */
function imagePixels(file) {
  const fd = fs.openSync(file, "r");
  try {
    const head = Buffer.alloc(24);
    fs.readSync(fd, head, 0, 24, 0);
    if (head[0] === 0x89 && head.toString("ascii", 1, 4) === "PNG") {
      return { w: head.readUInt32BE(16), h: head.readUInt32BE(20) };
    }
    if (head[0] !== 0xff || head[1] !== 0xd8) throw new ExecError(`unrecognized raster format: ${file}`);
    // Walk JPEG segments to the frame header. SOF0/1/2/3/5..7/9..11/13..15
    // carry the dimensions; DHT/DQT and the rest are skipped by their length.
    const size = fs.fstatSync(fd).size;
    const seg = Buffer.alloc(9);
    for (let at = 2; at + 9 <= size; ) {
      fs.readSync(fd, seg, 0, 9, at);
      if (seg[0] !== 0xff) throw new ExecError(`malformed JPEG at byte ${at}: ${file}`);
      const marker = seg[1];
      if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
        return { w: seg.readUInt16BE(7), h: seg.readUInt16BE(5) };
      }
      if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) { at += 2; continue; }
      at += 2 + seg.readUInt16BE(2);
    }
    throw new ExecError(`JPEG carries no frame header: ${file}`);
  } finally {
    fs.closeSync(fd);
  }
}

/**
 * Shrink a raster until it fits the single-message budget.
 *
 * A 5K display captures to ~22MB of PNG, which is ~29MB of base64 — past every
 * host limit, so the alternative is handing back a receipt with no picture and

View on GitHub (pinned to 73e0f67d83)