Skip to content

fix(site/src/pages/AgentsPage/components): derive diff cache keys from patch content - #27982

Closed
DanielleMaywood wants to merge 2 commits into
mainfrom
danielle/diff-content-cache-keys
Closed

fix(site/src/pages/AgentsPage/components): derive diff cache keys from patch content#27982
DanielleMaywood wants to merge 2 commits into
mainfrom
danielle/diff-content-cache-keys

Conversation

@DanielleMaywood

Copy link
Copy Markdown
Contributor

Problem

Since the @pierre/diffs bump to 1.3.3 (#27932), the chat UI intermittently shows a red error box instead of a tool diff:

DiffHunksRenderer.processDiffResult: deletionLine and additionLine are null, something is wrong

The 1.3.x web components default each parsed diff's cacheKey to the file name, and the singleton worker pool caches highlighted ASTs by that key alone. Two different diff bodies for the same path (e.g. consecutive edit_files turns on one file, or a re-render after a later edit) therefore share one cache entry, and rendering the newer diff against the older highlighted AST throws inside the renderer. Upstream tracks this as pierrecomputer/pierre#1052; #964 documents the same throw path. The existing RemoteDiffPanel already works around it by passing a unique cacheKeyPrefix, but the chat tool renderers and LocalDiffPanel parse without one.

Fix

Derive the cacheKeyPrefix from the patch text (FNV-1a checksum) so keys are specific to the exact diff body while staying stable across re-renders and remounts:

  • parseSingleFileDiff (shared by the write_file diff builder, the synthetic edit_files builder, and the server-supplied diff parser) now passes a content-derived prefix.
  • useParsedDiff falls back to the same prefix when a caller supplies none, which also fixes LocalDiffPanel. Explicit prefixes (RemoteDiffPanel's timestamp-scoped key) still win.

A 32-bit checksum is deliberate: the key only needs to separate different patch bodies for the same path while an older AST is cached, and crypto.subtle is async so it cannot run in the render path.

The first commit is an unrelated prerequisite: main is currently not make gen/make fmt clean (its gen and fmt CI jobs are red), and the mandatory pre-commit hook fails on any unstaged change. That drift is a mechanical follow-up to 07f79af, which sanitized notification labels before template rendering but regenerated only the Docker-free goldens; the AI budget templates' eq .Labels.limit_source "user_override" condition no longer matches because SanitizeMarkdown escapes the underscore. Kept as a separate commit so it can be dropped or split out.

Root-cause trace (agent tool call to renderer crash)
  1. Chat agent invokes edit_files (coderd/x/chatd/chattool/editfiles.go), which calls the workspace agent's POST /api/v0/edit-files with IncludeDiff: true.
  2. The agent applies edits and computes a unified diff per file with go-udiff (agent/agentfiles/files.go), returned as FileEditResponse.Files[].Diff.
  3. chatd stores the tool result as a JSON tool-result message part; the frontend pairs it with the tool call (messageParsing.ts).
  4. Tool.tsx dispatches to EditFilesRenderer, which parses the server diff (or synthesizes one from the args via Diff.createPatch) through parseSingleFileDiff in ChatElements/tools/utils.ts. Before this fix, parsePatchFiles was called without a cacheKeyPrefix, so the parsed FileDiffMetadata had no cacheKey.
  5. EditFilesTool.tsx renders <FileDiff fileDiff=...>. The @pierre/diffs component assigns cacheKey = fileDiff.name when absent (components/FileDiff.js:472).
  6. DiffHunksRenderer.renderDiff consults the singleton worker pool's highlight cache keyed only by that cacheKey (worker/WorkerPoolManager.js:76-77, entries stored at :653/:701).
  7. A stale hit returns the highlighted AST built for the earlier diff of the same path; processDiffResult walks the new hunks and indexes the old, shorter code.additionLines/deletionLines arrays (renderers/DiffHunksRenderer.js:646-651), both undefined, and throws. The component catches it and renders the error box via applyErrorToDOM.

Reproduced locally against the shipped 1.3.3 library by seeding the pool cache with a first diff's result and rendering a second diff of the same path; with content-derived keys the lookup misses and rendering succeeds.


Generated with Coder Agents.

Follow-up to 07f79af (markdown rendering improvements), which sanitized notification labels before template rendering but regenerated only the Docker-free golden files. The AI budget templates conditionally render 'This limit is a per-user override.' on eq .Labels.limit_source "user_override"; SanitizeMarkdown escapes the underscore to user\_override, so the condition no longer matches and the committed goldens are stale. CI's gen job restores coderd/notifications/testdata instead of regenerating (it needs a DB), which is why main's gen and fmt jobs are red. Regenerate the four AI budget goldens with the golden test and apply gci's import ordering to smtp_internal_test.go.
…m patch content

@pierre/diffs 1.3.x defaults each parsed diff's cacheKey to the file name, and the singleton worker pool caches highlighted ASTs by that key alone. Two different diff bodies for the same path (e.g. consecutive edit_files turns on one file) therefore share a cache entry, and rendering the newer diff against the older highlighted AST throws DiffHunksRenderer.processDiffResult: deletionLine and additionLine are null. Pass a content-derived (FNV-1a) cacheKeyPrefix from parseSingleFileDiff and useParsedDiff so keys are specific to the exact patch body while staying stable across re-renders.

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Chat: Review in progress (0/1 reviewers complete) | View chat
Requested: 2026-08-10 11:54 UTC by @DanielleMaywood

deep-review v0.9.0 | Round 1 | 95e8b71..3aafaa0

Status: Netero (first pass)

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@DanielleMaywood
DanielleMaywood marked this pull request as draft August 10, 2026 11:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3aafaa09c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +29 to +32
let hash = 0x811c9dc5;
for (let i = 0; i < text.length; i++) {
hash ^= text.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a collision-resistant patch cache key

When two different patches for the same path produce the same 32-bit FNV-1a value, they still receive identical cache keys and can reproduce the stale-AST renderer failure this change is meant to prevent. This is concrete rather than hypothetical: otherwise identical file.ts patches replacing old with 3frRhAsv2W and kDMJTx1ES3 both produce content-119c7773. If the first AST remains in the singleton worker pool, rendering the second patch reuses it, so use a substantially wider synchronous digest or a collision-free content identity.

Useful? React with 👍 / 👎.

@DanielleMaywood
DanielleMaywood deleted the danielle/diff-content-cache-keys branch August 10, 2026 11:58
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant