Hmbown/CodeWhale · error · ExecError

JPEG carries no frame header

Error message

JPEG carries no frame header: ${file}

What it means

After walking all JPEG segments without encountering an SOFn frame header (0xC0–0xCF excluding DHT/C8/CC variants), `imagePixels` gives up with `JPEG carries no frame header: <file>`. A valid baseline or progressive JPEG must contain exactly one such frame header, so its absence means dimensions cannot be determined.

Solutions

  1. Re-capture the screenshot; this file has no decodable frame header.
  2. Validate with an image tool (`sips -g pixelHeight file`) before using imagePixels.
  3. Fall back to capturing dimensions from the capture command output instead of parsing the file.
  4. Check the writer that produced the file — a short write likely truncated it before the frame header.

Example fix

// before\nconst { w, h } = imagePixels(path); // throws if SOF missing\n// after\ntry { const { w, h } = imagePixels(path); }\ncatch (e) { const { width, height } = dimsViaSips(path); }
Defensive patterns

Strategy: fallback

Validate before calling

const out = childProcess.execFileSync("sips", ["-g", "pixelWidth", "-g", "pixelHeight", file], {encoding: "utf8"});\nif (!/pixelWidth:\s*\d+/.test(out)) throw new Error("undecodable image: " + file);

Try / catch

try { return imagePixels(file); } catch (e) { if (/no frame header/.test(e.message)) return dimsViaSips(file); throw e; }

Prevention

When it happens

Trigger: A JPEG missing its SOF marker — heavily truncated files where the data ends before the frame header is reached, or non-image data that happens to begin with 0xFF 0xD8, or a file whose segment lengths are wrong so the walk terminates early.

Common situations: Zero-dimension or degenerate capture output, files that pass the two-byte magic sniff but are not real JPEGs, or truncated downloads/recordings used as screenshots.

Related errors


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

Appendix: source

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

    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
 * a screenshot tool that never shows anything. Downscaling here, before the
 * caller reads the PNG header, keeps coordinates exact by construction:
 * `pixels` comes from the header, `points` stays in screen points, and `scale`
 * is derived from the two, so raster-to-point conversion follows automatically.
 *
 * PNG bytes track pixel count, so the long edge shrinks by the square root of
 * the overshoot. The estimate is verified rather than trusted — screen content
 * compresses unevenly — and gives up rather than shrinking past legibility.

View on GitHub (pinned to 73e0f67d83)