Skip to content

Invalidate cached local-file images in AI blocks when the file changes#12840

Open
zachlloyd wants to merge 1 commit into
masterfrom
oz-agent/blocklist-image-cache-invalidation
Open

Invalidate cached local-file images in AI blocks when the file changes#12840
zachlloyd wants to merge 1 commit into
masterfrom
oz-agent/blocklist-image-cache-invalidation

Conversation

@zachlloyd

@zachlloyd zachlloyd commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a caching bug in inline image display in AI blocks (the PR #9993 feature): if an agent displays a local image, the file is then changed on disk, and the agent is asked to display the same path again, the second display showed the stale, first-loaded image instead of the updated file. This made it impossible to iterate on an image with the agent.

Root cause: AssetSource::LocalFile was keyed by path only. AssetCache::load_asset() reads a path once and never re-reads it, and the rendered-size ImageCache is keyed on the same source. There was no mtime/content check and no invalidation path for LocalFile (eviction only covered AssetSource::Raw), so a changed file at the same path kept hitting the cached decode.

Fix: include an optional content version in the local-file cache key.

  • New LocalFileContentVersion (a NonZeroU64 hash of last-modified time + file size) and a content_version: Option<LocalFileContentVersion> field on AssetSource::LocalFile (crates/warpui_core/src/assets/asset_cache.rs). Because it is part of the source, it flows into both the AssetCache (decoded bytes) and ImageCache (rendered) keys, so an edited file misses the cache and is re-read.
  • AssetSource::with_local_file_content_version() reads the metadata; the AI block calls it inside the existing background link-detection task (app/src/ai/blocklist/block.rs), which populates the per-block resolved_blocklist_image_sources map.
  • Stored as NonZeroU64 so Option<LocalFileContentVersion> stays pointer-width (niche) and AssetSource does not grow (a {u128,u64} version tripped clippy::large_enum_variant on an unrelated enum via WarpTheme).

Metadata is read only when the block resolves its image sources (block creation / output completion / restore / shell-change) — never on the per-streaming-token path and never on render. Renders reuse the cached resolved source, and the render-time fallback stays stat-free (content_version: None). The stat also runs in the existing background spawn_blocking task, off the UI thread.

This is a general AssetCache/ImageCache improvement; non-blocklist callers (themes, notebooks) keep path-only caching (content_version: None), so their behavior is unchanged.

https://www.loom.com/share/c0eab28c73f146bb9ba531d3ba5b8081

Linked Issue

#12802

  • The linked issue is labeled ready-to-spec or ready-to-implement.

Testing

  • Added 3 focused unit tests in crates/warpui_core/src/assets/asset_cache_tests.rs:

    • a changed file (different size) produces a different cache key, while an unchanged file produces the same key
    • for_path returns None for a missing file
    • with_local_file_content_version() leaves non-local sources unchanged
  • cargo fmt -- --check, cargo check, and cargo clippy --tests -- -D warnings are clean for the changed crates and the app (warp with local_fs); the new unit tests and the existing tests I touched pass.

  • Could not run in this cloud sandbox: the full app test binary OOMs while linking (memory limit — it compiles fine), and the full wasm bundle build needs clang plus app-level feature wiring unavailable here. The wasm changes are cfg-gated stubs (for_path returns None on wasm). CI will run the full presubmit.

  • I have manually tested my changes locally with ./script/run

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-BUG-FIX: Re-displaying a local image in an AI response now reflects edits made to the file on disk instead of showing a stale cached copy.


Conversation: https://staging.warp.dev/conversation/970500d6-1824-42c3-85a4-620b31258e19
Run: https://oz.staging.warp.dev/runs/019ed6bb-b701-78a8-8fa2-844979f25593
This PR was generated with Oz.

Local image files were cached by path only, with no modification-time or content check and no invalidation path, so re-displaying the same path in an AI response always returned the first-loaded image even after the file changed on disk.

Add a LocalFileContentVersion (a NonZeroU64 hash of mtime + size) to AssetSource::LocalFile so the content version is part of both the decoded-bytes (AssetCache) and rendered (ImageCache) cache keys. The blocklist resolves it via AssetSource::with_local_file_content_version() inside the existing background link-detection task, so file metadata is read when the block resolves its image sources (creation/output completion), not on every render. Storing it as a NonZeroU64 keeps Option<LocalFileContentVersion> pointer-width so AssetSource does not grow.

Co-Authored-By: Oz <oz-agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Jun 19, 2026
@zachlloyd zachlloyd requested a review from kevinyang372 June 19, 2026 21:25
@zachlloyd zachlloyd marked this pull request as ready for review June 19, 2026 21:26
@oz-for-oss

oz-for-oss Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

@zachlloyd

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds an optional local-file content version to AssetSource::LocalFile and uses it for AI block image resolution so edited local images miss the existing path-only cache. The approach is wired through the blocklist image resolution path and leaves other callers on path-only caching.

Concerns

  • Versioned local-file sources create a new AssetCache/ImageCache key for each detected file rewrite, but the existing eviction logic only bounds AssetSource::Raw entries. Repeatedly iterating on a large image can retain every previous decoded/rendered version until the cache is torn down.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

/// sensitive to on-disk changes so an edited file is re-read instead of
/// served stale. `None` preserves path-only caching for callers that do
/// not need invalidation.
content_version: Option<LocalFileContentVersion>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Adding content_version makes each rewrite of the same local path a distinct cache key, but the cache only evicts Raw assets today. Repeated image iteration can retain all old decoded/rendered versions, so evict or replace older LocalFile entries for the same path when loading a new version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant