Skip to content

browser_screenshot saves JPEG bytes as image/png, corrupting the attachment (ATTACHMENT_CORRUPT + endless retries) #134

Description

@cyhano

Summary

browser_screenshot commits screenshot bytes to the host attachment store with a hard-coded image/png media type, even when the underlying capture actually produces JPEG bytes. This corrupts the stored attachment reference and, on replay, dsh-attachment-local throws AttachmentError("Stored attachment metadata does not match its reference.", "ATTACHMENT_CORRUPT"). The error is then surfaced to the LLM adapter as code: "UNKNOWN", which the retry policy treats as retryable, causing the same assistant turn to fail and retry 18 times before giving up.

Environment

  • Plugin: @wxg-prc-cpg/browser-skill-dsh-plugin v0.1.1
  • DeepSeek Harness (dsh) web profile
  • Browser: Microsoft Edge 151.0.4129.101
  • bsk CLI v0.1.10

Reproduction

  1. In a dsh web session on a model route with image input, invoke browser_screenshot.
  2. The plugin writes the capture bytes to ~/.dsh/attachments/v1/objects/… (content-addressed).
  3. Inspect the stored object — it is a JPEG (magic bytes ff d8 ff db, file reports JPEG image data), but the reference recorded in the session declares mediaType: "image/png".

Concrete observed object (sha256 = filename, so integrity is intact):

field reference value actual bytes
mediaType image/png image/jpeg
width 2048 2048 ✅
height 990 990 ✅
bytes 147937 147937 ✅

Root cause

mediaType is hard-coded to "image/png" at two places in the dsh plugin, without ever probing the actual captured bytes:

  • packages/dsh-plugin-browserskill/src/image.ts:58
    return await attachments.saveImage({ data, mediaType: "image/png", name });
  • packages/dsh-plugin-browserskill/src/tools.ts:845
    image: {
      attachmentId: String(ref.attachmentId),
      mediaType: "image/png" as const,
      bytes: ref.bytes,
      width: ref.width,
      height: ref.height,
      ...
    }

The capture path itself also assumes PNG end-to-end:

  • apps/extension/src/tools/observation.ts:239 calls captureVisibleTab(windowId, { format: "png" }), and the protocol ScreenshotResult.format is documented as "Always "png" in v0.1" (crates/bsk-protocol/src/tools/observation.rs:208) — but the actual bytes can still be JPEG (e.g. captureVisibleTab returning JPEG on some Chromium/Edge builds), so format: "png" is an unverified claim, not a detected fact.

Because the plugin declares image/png but hands real JPEG bytes to attachments.saveImage, the stored reference carries a mediaType that contradicts the bytes. On the read path, dsh-attachment-local's readImageFile verifies the sha256 (passes — bytes unchanged), then re-probes metadata and detects image/jpeg !== image/png, throwing ATTACHMENT_CORRUPT.

Impact

  • The screenshot attachment becomes permanently unreadable for the model.
  • The failure is reported to the adapter as code: "UNKNOWN" instead of ATTACHMENT_CORRUPT, so the retry policy (always mode) keeps retrying — 18 retries observed, ~27–30 s apart, ~8 minutes wasted — before the turn finally errors out.

Suggested fix

Stop trusting the declared "image/png". Either:

  1. Probe the actual bytes (e.g. with sharp) and use the detected media type both when calling saveImage and when assembling the returned reference (ref.mediaType already carries the correct post-normalization type — use it instead of "image/png" as const); or
  2. If the capture source cannot guarantee PNG, drop the assumption at the source: detect/report the real format from captureVisibleTab / CDP Page.captureScreenshot rather than hard-coding format: "png" in ScreenshotResult.

At minimum, tools.ts:845 should use ref.mediaType (the value saveImage actually returned) rather than re-declaring "image/png".

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions