moeru-ai/airi · error · Error
Screenshot must be PNG or JPEG.
Error message
Screenshot must be PNG or JPEG.
What it means
After reading the file, readImage() sniffs magic bytes: PNG (89 50 4E 47 0D 0A 1A 0A) or JPEG (FF D8 FF). Any other content is rejected because the returned data URL hardcodes image/png or image/jpeg and downstream consumers (model vision input) require those formats.
Solutions
- Let the AIRI CLI produce the screenshot (it emits PNG/JPEG) instead of placing your own file in the store
- Convert the image to PNG or JPEG before calling readImage
- Check the first bytes (PNG/JPEG magic) before calling to fail early
Example fix
// before
await copyFile('capture.webp', join(storeRoot, 'capture.png'))
await readImage({ path: join(storeRoot, 'capture.png') })
// after
const jpegPath = await sharp('capture.webp').jpeg().toFile(join(storeRoot, 'capture.jpg'))
await readImage({ path: jpegPath }) Defensive patterns
Strategy: validation
Validate before calling
import { readFile } from 'node:fs/promises'
async function isPngOrJpeg(p: string): Promise<boolean> {
const b = (await readFile(p)).subarray(0, 3)
const png = b[0] === 137 && b[1] === 80 && b[2] === 78
const jpeg = b[0] === 255 && b[1] === 216 && b[2] === 255
return png || jpeg
} Type guard
function hasImageMagic(bytes: Uint8Array): boolean {
const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71
const jpeg = bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255
return png || jpeg
} Try / catch
try {
return await readImage({ path })
} catch (e) {
if (e instanceof Error && e.message === 'Screenshot must be PNG or JPEG.') {
const converted = await convertToPng(path)
return readImage({ path: converted })
}
throw e
} Prevention
- Only read files produced by the AIRI computer-use CLI
- Sniff magic bytes before calling; do not trust file extensions
- Convert WebP/BMP/GIF inputs to PNG/JPEG first
When it happens
Trigger: The file inside the store is a WebP/BMP/GIF, an empty file, a partially written download, or a text/error file instead of a real screenshot.
Common situations: Another tool wrote into the store directory; a truncated capture from a crashed process; renaming a .webp to .png and expecting readImage to accept it.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- A motion sample occurs after the recording duration.
- Aliyun NLS credentials are incomplete.
- Archive entry not found
- audio models upstream returned malformed body
- audio voices upstream returned malformed body
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/c27f1a63add8909d.
Report an issue: GitHub.
Appendix: source
Thrown at apps/stage-tamagotchi/src/main/services/airi/computer-use/runtime.ts:151
})
}
async function readImage(input: unknown): Promise<string> {
const { path } = v.parse(v.object({ path: v.string() }), input)
return enqueue(async () => {
const root = await realpath(options.storeRoot)
const target = await realpath(path)
const within = relative(root, target)
if (!within || isAbsolute(within) || within === '..' || within.startsWith(`..${sep}`))
throw new Error('Only screenshots from this AIRI computer-use store can be read.')
const info = await stat(target)
if (!info.isFile() || info.size > 8 * 1024 * 1024)
throw new Error('Screenshot must be a file smaller than 8 MiB.')
const data = await readFile(target)
const png = data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
const jpeg = data[0] === 255 && data[1] === 216 && data[2] === 255
if (!png && !jpeg)
throw new Error('Screenshot must be PNG or JPEG.')
return `data:image/${png ? 'png' : 'jpeg'};base64,${data.toString('base64')}`
})
}
/** Rejects queued calls, aborts active work, and waits for the owned daemon to exit. */
async function dispose() {
disposed = true
abort.abort()
await queue
try {
await daemon?.stop()
}
finally {
await cleanupSocket()
}
}
return { run, readImage, dispose }View on GitHub (pinned to 438a067dde)