Repository navigation
Delete chat attachments with their chats and sweep orphans (OR-574) - #755
Conversation
A refused project delete (busy run artifacts, data-dir move, runs in flight, artifact cleanup failure, or contended native lock) no longer kills OpenCode history while the chat rows survive. The store now takes the native-deletion lock only after the run-artifacts guard and move check, then cleans artifacts, purges native sessions, and deletes rows, so the native lock is never held across the run-artifacts wait. Part of OR-569. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat and project deletes now remove the attachment files their messages showed once no remaining message shows them, skipping while a data-dir move is claimed. The periodic reclaim task also sweeps att-* files no message shows after a 24h settle, within the orphan-pass budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The changes affect local chat and project cleanup within the app. Fix the race between attachment cleanup and storage moves before merging. FindingsFix with agent prompt### Issue 1
src/store.rs:2689-2691
`cleanup_orphaned_chat_attachments` checks the move claim but holds no shared lock while deleting files. If another dashboard starts a storage move after that check, cleanup can remove a source file while the move copies it. `copy_tree` then fails on the missing file, or the final size check fails.
Hold a lock that prevents a move from starting until cleanup finishes. Apply the same protection to `remove_unreferenced_chat_attachments`.
### Issue 2
src/store.rs:2698-2702
Every sweep starts `read_dir` from the beginning. If checking retained files in a large folder or on a slow disk consumes the five-second budget, the sweep stops before reaching later orphan files. Each retry checks the same prefix, so those orphans can remain forever while the loop retries every second.
Keep progress between passes or rotate the starting point, and add a test for budget exhaustion.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryAdds attachment cleanup after chat and project deletion, plus a periodic sweep for unreferenced files older than 24 hours.
|
| if self.data_dir_move_claimed()? { | ||
| return Ok(false); | ||
| } |
There was a problem hiding this comment.
Cleanup can break storage moves
cleanup_orphaned_chat_attachments checks the move claim but holds no shared lock while deleting files. If another dashboard starts a storage move after that check, cleanup can remove a source file while the move copies it. copy_tree then fails on the missing file, or the final size check fails.
Hold a lock that prevents a move from starting until cleanup finishes. Apply the same protection to remove_unreferenced_chat_attachments.
Knowledge Base Used: Workspace state and file synchronization
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store.rs
Line: 2689-2691
Comment:
**Cleanup can break storage moves**
`cleanup_orphaned_chat_attachments` checks the move claim but holds no shared lock while deleting files. If another dashboard starts a storage move after that check, cleanup can remove a source file while the move copies it. `copy_tree` then fails on the missing file, or the final size check fails.
Hold a lock that prevents a move from starting until cleanup finishes. Apply the same protection to `remove_unreferenced_chat_attachments`.
**Knowledge Base Used:** [Workspace state and file synchronization](https://app.greptile.com/alphaxiv/-/custom-context/knowledge-base/alphaxiv/openresearch/-/docs/workspace-state-and-files.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in d1cc352: Store::hold_off_data_dir_move clears stale leases, then takes a shared lock on the data-dir move lock (a move takes it exclusively before claiming its lease and keeps it for the whole move) and re-checks the lease. Both the sweep and delete-time removal hold it while removing, so a move from another dashboard fails fast instead of racing the removals. Test: a move cannot acquire its lock while the hold is held.
| let mut started = std::time::Instant::now(); | ||
| let mut referenced = None; | ||
| for entry in entries.flatten() { | ||
| if started.elapsed() >= ORPHAN_PASS_BUDGET { | ||
| return Ok(true); |
There was a problem hiding this comment.
Later orphan files stay untouched
Every sweep starts read_dir from the beginning. If checking retained files in a large folder or on a slow disk consumes the five-second budget, the sweep stops before reaching later orphan files. Each retry checks the same prefix, so those orphans can remain forever while the loop retries every second.
Keep progress between passes or rotate the starting point, and add a test for budget exhaustion.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store.rs
Line: 2698-2702
Comment:
**Later orphan files stay untouched**
Every sweep starts `read_dir` from the beginning. If checking retained files in a large folder or on a slow disk consumes the five-second budget, the sweep stops before reaching later orphan files. Each retry checks the same prefix, so those orphans can remain forever while the loop retries every second.
Keep progress between passes or rotate the starting point, and add a test for budget exhaustion.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in d1cc352: the budget now starts at the first removal, matching the run-artifact sweep (classification unbudgeted). Retained files can no longer exhaust it, and removed files leave the next listing, so every exhausted pass makes progress. Test: with a zero budget each pass removes exactly one orphan until none remain.
# Conflicts: # src/store.rs
Attachment removal now holds a shared lock on the data-dir move lock, which a move takes exclusively before claiming its lease, so another dashboard cannot start copying mid-removal. Only removals count against the sweep budget, so a pass that walks many retained files still reaches later orphans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Chat attachments (
data_dir/chat-attachments/att-<uuid>__<name>.<ext>) were never deleted.{"type":"image","text":<file>}) in user messages'parts_json.prepared_inputonly names files whose user message is in the same session.Store::delete_chat_sessionanddelete_local_project_withcollect the names their messages show inside the delete transaction.cleanup_orphaned_chat_attachmentsruns in the existingreclaim_orphaned_run_artifactsloop, under the same gate, as its ownspawn_blockingwith its own log line.att-*regular files that no message shows and that are older than 24h. Uploads are written before their message row.ORPHAN_PASS_BUDGETand skips per-file errors.ATTACHMENTS_DIRandis_attachment_namenow live next tosave_images.Stacked on #753. Retarget to
mainafter it merges.Closes OR-574
Testing
cargo fmt --all --check,cargo clippy --all-targets -- -D warnings,cargo build --locked,cargo test --lockedatt-name in an image part is never removed.chat-attachments/.orx upstarts.🤖 Generated with Claude Code