Deleting a duplicate could remove the file from disk while leaving the
entry in the cache, so the duplicate kept showing after a refresh and
only Rebuild Cache cleared it (issue #1154). Three paths produced that
state:
* ModelHashIndex.remove_by_path() deleted _hash_to_path[hash_val] with no
guard. The delete flow passes the cache row's sha256 as a hint; once
the row and the index had drifted apart (a hash corrected in place, a
partially applied cache update) the hint was not in the index and the
KeyError aborted _batch_update_cache_for_deleted_models(), so every
deleted model stayed in memory and in SQLite.
* The same cache update was skipped outright when the scanner's
cancellation flag was set, turning files staged before the cancellation
into ghost entries.
* A file that was already gone from disk could never be cleaned up:
staging found no artifacts, the hard delete reported nothing, and the
row survived as a permanent "No files deleted" failure.
Make the hash hint advisory and guard the index deletions; drop the
path-keyed entries (filename duplicates, autov2/autov3) even when no hash
mapping exists. Always apply the cache update after a delete, purge stale
index paths that never reached the cache, and treat a missing file under
a reachable folder as deleted while still keeping unreachable roots
offline. bulk_delete_models() reports success: false when nothing was
removed instead of presenting a no-op as a completed delete.
The duplicates UI now surfaces what it used to hide: per-file failures
from `results`, and a cache-update failure that needs Rebuild Cache.