fix(copilot): disambiguate VFS upload paths to prevent stale-row reads#4454
fix(copilot): disambiguate VFS upload paths to prevent stale-row reads#4454TheodoreSpeaks merged 2 commits intostagingfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview
Upload lookup/listing is updated to resolve by Reviewed by Cursor Bugbot for commit 3ba5928. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR fixes a real production bug where same-named chat attachments collided on the VFS path
Confidence Score: 4/5Safe to merge after addressing the UPDATE guard in The core logic in
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[buildCopilotRequestPayload
called for chat attachment] --> B[trackChatUpload]
B --> C{UPDATE existing row
by S3 key?}
C -->|row found| D{23505 unique
violation?}
C -->|no row found| E[INSERT new row
displayName = candidate]
D -->|no| F[return displayName]
D -->|yes - displayName taken| G[increment n
retry with suffixedName]
E --> H{INSERT 23505?}
H -->|no| F
H -->|yes| G
G --> C
F --> I[payload hint:
read uploads/displayName]
I --> J[findMothershipUploadRowByChatAndName
exact match on displayName]
J --> K[read correct file
never a stale row]
Reviews (1): Last reviewed commit: "fix(copilot): disambiguate VFS upload pa..." | Re-trigger Greptile |
45f0a6c to
a32f1e3
Compare
a32f1e3 to
658dc6f
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 658dc6f. Configure here.
Same-name chat attachments (e.g. multiple "image.png" screenshots) collided
on the VFS path uploads/<originalName>. The agent's read("uploads/image.png")
returned whichever row Postgres ordered first — leading to the agent reading
stale uploads instead of the latest one (real prod incident: trace
71f719d884194b6820851e2267b98cda).
Adds a nullable display_name column on workspace_files plus a partial unique
index on (chat_id, display_name) WHERE context='mothership' that spans the
row's entire lifetime (NOT gated on deleted_at IS NULL). VFS paths handed to
the LLM during a session must remain stable — soft-deleting a sibling cannot
free a name slot the model has already been told about, since that would
make read("uploads/<name>") silently resolve to a different file.
Migration is a clean two-statement ADD COLUMN + CREATE UNIQUE INDEX with no
backfill. Legacy rows keep display_name = NULL; readers coalesce to
original_name. Pre-existing collisions in legacy data persist (the upload
already happened — no fix possible without rewriting history) but new
uploads get full disambiguation. NULLs are distinct in PG unique indexes,
so legacy rows don't block index creation or new inserts.
trackChatUpload allocates "image.png", then "image (2).png", "image (3).png",
... with retry on 23505. Returns the assigned displayName so the read-hint
string given to the model uses it. Resolver matches displayName for new rows
and falls back to original_name for legacy NULL rows, preserving the
normalize-segment fallback for macOS U+202F screenshot names.
658dc6f to
288f023
Compare
Same-name chat attachments (e.g. multiple "image.png" screenshots) collided
on the VFS path uploads/<originalName>. The agent's read("uploads/image.png")
returned whichever row Postgres ordered first — leading to the agent reading
stale uploads instead of the latest one (real prod incident: trace
71f719d884194b6820851e2267b98cda).
Adds a nullable display_name column on workspace_files plus a partial unique
index on (chat_id, display_name) WHERE context='mothership' that spans the
row's entire lifetime (NOT gated on deleted_at IS NULL). VFS paths handed to
the LLM during a session must remain stable — soft-deleting a sibling cannot
free a name slot the model has already been told about, since that would
make read("uploads/<name>") silently resolve to a different file.
Migration is a clean two-statement ADD COLUMN + CREATE UNIQUE INDEX with no
backfill. Legacy rows keep display_name = NULL; readers coalesce to
original_name. Pre-existing collisions in legacy data persist (the upload
already happened — no fix possible without rewriting history) but new
uploads get full disambiguation. NULLs are distinct in PG unique indexes,
so legacy rows don't block index creation or new inserts.
trackChatUpload allocates "image.png", then "image (2).png", "image (3).png",
... with retry on 23505 narrowed to the displayName index (other 23505s, e.g.
key-active collisions from concurrent same-s3Key inserts, rethrow). Returns
the assigned displayName so the read-hint string given to the model uses it.
Resolver matches displayName for new rows, falls back to original_name for
legacy NULL rows, and orders by uploaded_at DESC so legacy ambiguity resolves
to the newest upload (which fixes the original prod symptom even pre-fix).
e451060 to
3ba5928
Compare

Summary
image.pngscreenshots) collided on the VFS pathuploads/<originalName>. The agent'sread("uploads/image.png")returned whichever row Postgres ordered first (noORDER BY,LIMIT 1), causing it to read stale uploads instead of the latest one (real prod incident: trace71f719d884194b6820851e2267b98cda).display_namecolumn onworkspace_files+ partial unique index on(chat_id, display_name) WHERE context='mothership'. Stable per-row for the row's lifetime — paths don't shift even if rows get hard-deleted.trackChatUploadallocates a freedisplayNameat insert time (image.png, thenimage (2).png,image (3).png, ...) with retry-on-23505 against the unique index. Returns the assigned name so the read-hint string given to the model uses it.displayNameand keeps thenormalizeVfsSegmentfallback for macOS U+202F vs ASCII space in screenshot filenames.(chat_id, original_name)in upload order; non-mothership rows getdisplay_name = original_name. ThenSET NOT NULLand create the partial unique index.Type of Change
Testing
apps/sim/lib/copilot/tools/handlers/upload-file-reader.test.ts(8 cases — exact match, suffixed match, U+202F fallback, list ordering, read flow) andapps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts(9 cases —suffixedNamefor extensions/extensionless/dotfiles, UPDATE-existing path, INSERT path, 23505 retry, non-unique error rethrow).vfs.test.tsandworkspace-file-manager.test.tsstill green.bun run lint,bun run type-check,bun run check:api-validation:strictall pass.Checklist