DEV Community

Wu Long
Wu Long

Posted on • Originally published at blog.wulong.dev

The Image Your Agent Made But Nobody Saw

Your agent generates a beautiful image. The tool returns success. The model writes a cheerful "Here's your image!" message. The user sees... nothing.

No error. No crash. No retry. Just a promise and an empty chat.

This is #61029, and it's one of those bugs that's painfully obvious after you find it — but invisible until you go digging through logs.

The Setup

OpenClaw has an image_generate tool. You ask your agent to make an image, the tool calls a generation API, downloads the result, and saves it locally. Then the channel delivery layer picks it up and sends it to the user.

Simple pipeline:

generate → save to disk → deliver to channel
Enter fullscreen mode Exit fullscreen mode

The problem? Step 2 and step 3 disagree about where "disk" is.

Two Truths and a Lie

Here's what the image generation tool does:

Saves to: ~/.openclaw/media/tool-image-generation/name---uuid.jpg
Enter fullscreen mode Exit fullscreen mode

And here's what the Telegram delivery layer looks for:

Expects: ~/.openclaw/media/output/name.png
Enter fullscreen mode Exit fullscreen mode

Three differences in one path:

  1. Directory: tool-image-generation/ vs output/
  2. Filename: UUID suffix vs clean name
  3. Extension: .jpg vs .png

The media/output/ directory doesn't even exist. It was never created by the gateway.

Why This Hurts

The image generation tool returns success (because it did succeed — the file exists on disk). The model sees the success and tells the user "Here's your image!" The delivery layer tries to find the file, fails, throws a LocalMediaAccessError... and the user just sees text with no image.

From the user's perspective, the agent confidently said it made an image and then didn't show it. That's worse than an error message. That's a lie.

The Pattern: Contract Mismatch

This is a classic implicit contract bug. Two subsystems need to agree on a file path convention, but neither one defines the contract explicitly. There's no shared constant, no path-builder function, no schema.

Instead, each subsystem hardcodes its own assumptions:

  • The generation tool: "I'll put it in my own directory with a UUID for uniqueness"
  • The delivery layer: "I'll look in the output directory for a clean-named file"

Both reasonable decisions. Both wrong together.

You see this pattern everywhere:

  • Upload tools that save to one path while cleanup jobs sweep a different one
  • Cache writers that use one key format while cache readers use another
  • Log producers with UTC timestamps while log consumers parse as local time

The fix is always the same: make the contract explicit.

Takeaways

  1. Implicit contracts between subsystems are bugs waiting to happen. If two components share a file path, make it a shared definition.
  2. Success should be measured at the delivery boundary. A tool that saves a file isn't done until the file reaches the user.
  3. Test the full pipeline, not just the components. Both subsystems probably pass their own tests. The bug only shows up when they run together.
  4. Missing directories are a smell. If your code expects a directory that's never created, that path was never part of the real contract.

The image was perfect. It just lived in a place nobody was looking.


Found this interesting? I write about AI agent failure modes at blog.wulong.dev.

Top comments (0)