[Bug]: Re-embedding an overwritten local image shows the old cached version#15676

opened19d agobyBbtsouth

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

No open issue covers this. #13942 and #14009 addressed the same problem and were closed under the prior-approval rule, so this issue is for maintainers to decide the intended behavior.

Area

apps/server, apps/web

Actual behavior

An agent regenerated an image by overwriting the same PNG it had embedded earlier in the thread, then embedded that path again in its reply. The new message showed the old image. Opening the same file from the files panel showed the correct, updated image, so the two views disagreed side by side.

The file on disk was correct, and the agent had read the new file back before replying.

Steps to reproduce

  1. Have an agent write an image to a path, for example /tmp/chart.png, and embed it in a reply with ![chart](/tmp/chart.png).
  2. Within an hour, have it overwrite that file with different content and embed the same path in a new reply.
  3. The new reply shows the previous image.
  4. Open the file from the files panel. It shows the current image.

Expected behavior

A message that embeds a local image shows the file as it was when the message was written, or at least never shows an older version than the one on disk.

Cause

Two caches key only on the path:

  • The client reuses the signed asset URL per resource path for up to an hour (useAssetUrlState, ASSET_URL_IDLE_TTL_MS in packages/client-runtime/src/state/assets.ts).
  • The server serves images with Cache-Control: private, max-age=3600 (assetResponseHeaders in apps/server/src/http.ts). The token does not include the file's mtime or size.

Audio and video already get no-store because "host media can change in place", but images do not.

Confirmed on main at 4ee6bfd5.

Open question

The closed PRs show the possible directions:

  1. Treat local images as live references and revalidate them (for example no-cache with an mtime/size ETag). Small change, but older messages would also switch to the newest file.
  2. Snapshot referenced images per message (#14009). Preserves history, but adds storage and retention.
  3. Prompt-only guidance to write revisions to new file names (#13942).

Which behavior is intended?

Impact

Minor bug. Misleading when an agent iterates on an image or screenshot, because the chat shows a result that does not match the file.

Version or commit

0.0.46-nightly.20261004.2648. Cause confirmed on main at 4ee6bfd5.

Environment

Linux, T3 desktop app, Claude provider.

Workaround

Reload the window, open the file from the files panel, or have the agent write each revision to a new file name.

Note

Grok responding on behalf of Julius.

Triage

Thanks for the write-up, @btsouth, and for linking the two closed PRs, which give useful context.

What I found

Confirmed on main at 4ee6bfd50e.

  • Chat reuses a cached URL. Chat embeds a local image as a media-file resource and renders whatever useAssetUrlState already has (ChatMarkdownAssetImage in apps/web/src/components/ChatMarkdown.tsx). That cache is keyed by the resource, not by the file's contents.
    • While the thread stays open, the signed URL refreshes every 30 minutes.
    • A new subscriber doesn't refetch during the 5-minute stale window.
    • An unused entry lives for an hour.
    • The timings are set in packages/client-runtime/src/state/assets.ts, and mobile uses the same hook.
  • The token doesn't track content. The signed claims are path, device, and inode, plus a one-hour expiry (finalizeAbsoluteMediaFileAsset in apps/server/src/assets/AssetAccess.ts). Overwriting a file in place keeps its inode, and the token carries no mtime or size. The old pixels come from reusing the URL.
  • Images are cacheable, unlike other media. Images get Cache-Control: private, max-age=3600 (assetResponseHeaders in apps/server/src/http.ts), while audio and video are rewritten to private, no-store in assetFileResponse.
  • Why the files panel shows the right image. WorkspaceImagePreview signs a workspace-file URL with a workspace-revision taken from the latest file change, command, or checkpoint (FilePreviewPanel.tsx, workspaceMutationId in ChatView.tsx). That skips both caches, and reloading has the same effect, which matches your workaround.
  • Files outside the project. Absolute paths like /tmp/chart.png still go stale in chat, but the files panel only resolves files inside the workspace root.

Likely fix area

The options line up with the ones you listed. Picking one is a decision about what an image path in a message should mean over time:

  • Revalidate like audio and video, using no-store, or no-cache with an mtime/size validator, and don't reuse a cached URL for a new message. This is a small change, but older messages would also show the newest file once their cached copy expires.
  • Snapshot the image when the message is written, as in #14009. This keeps history but adds storage and retention.
  • Have agents use a new file name for each revision, as in #13942. This avoids the cache but doesn't fix a reused path.

A maintainer will decide on the fix direction.

Reproduced on Linux in the web dev app at b5a226e49c, with Claude Sonnet 5.5. The agent created a red 100×100 PNG, embedded its absolute path, then overwrote it in place with green pixels and embedded the same path in a second reply. The new reply still displayed red: rendered pixel RGB 255,0,0; PNG on disk RGB 0,255,0. Reloading the page displayed green.

Reproduction capture.

Checked all 947 open issues. #13138 covers expanded image-read tool outputs; this report covers Markdown images in replies.

Fix proposed in #23212. The real web app shows stale bytes on b5a226e49c; paired recordings show the later reply fetching the overwritten image after the fix, without reload. Evidence and coverage limits.

At e76c41919d, 28 focused tests and web/mobile/client-runtime typechecks pass. Native iOS builds failed resolving GhosttyKit; Electron and Android are unverified. Streaming/reconnect and remaining fixture gaps are listed in the PR for maintainer review.