fix: three more correctness bugs from PR review

- upsert_image now resets ai_tagged_at, ai_rating, ai_tagger_model, and
  ai_tagger_error to NULL and deletes AI-sourced image_tags when a file
  is reindexed after content changes, preventing stale tags/ratings from
  the previous version being shown and blocking re-tagging
- get_pending_tagging_jobs_excluding now applies the paused-folder
  exclusion in SQL before LIMIT using folder_exclusion_clause, matching
  the pattern used by caption and embedding job claimers; previously a
  large paused-folder backlog could starve all unpaused folders
- Merged similarRequestToken into galleryRequestToken so all
  gallery-producing requests share one generation counter; a late
  similarity response can no longer overwrite a subsequently loaded
  folder, and vice versa
This commit is contained in:
2026-06-06 22:09:54 +01:00
parent de0c2ab12d
commit 9e08d9cce3
2 changed files with 29 additions and 17 deletions
+17 -7
View File
@@ -349,7 +349,11 @@ pub fn upsert_image(conn: &Connection, img: &ImageRecord) -> Result<i64> {
generated_caption = excluded.generated_caption, generated_caption = excluded.generated_caption,
caption_model = excluded.caption_model, caption_model = excluded.caption_model,
caption_updated_at = excluded.caption_updated_at, caption_updated_at = excluded.caption_updated_at,
caption_error = excluded.caption_error caption_error = excluded.caption_error,
ai_rating = NULL,
ai_tagger_model = NULL,
ai_tagged_at = NULL,
ai_tagger_error = NULL
RETURNING id", RETURNING id",
params![ params![
img.folder_id, img.folder_id,
@@ -385,6 +389,12 @@ pub fn upsert_image(conn: &Connection, img: &ImageRecord) -> Result<i64> {
], ],
|row| row.get(0), |row| row.get(0),
)?; )?;
// Remove stale AI tags when content changes so they aren't shown for the
// new file. User-sourced tags are preserved.
conn.execute(
"DELETE FROM image_tags WHERE image_id = ?1 AND source = 'ai'",
[id],
)?;
Ok(id) Ok(id)
} }
@@ -1678,14 +1688,17 @@ fn get_pending_tagging_jobs_excluding(
paused_folder_ids: &std::collections::HashSet<i64>, paused_folder_ids: &std::collections::HashSet<i64>,
limit: usize, limit: usize,
) -> Result<Vec<TaggingJob>> { ) -> Result<Vec<TaggingJob>> {
let mut stmt = conn.prepare( let sql = format!(
"SELECT j.image_id, i.folder_id, i.path "SELECT j.image_id, i.folder_id, i.path
FROM tagging_jobs j FROM tagging_jobs j
JOIN images i ON i.id = j.image_id JOIN images i ON i.id = j.image_id
WHERE j.status = 'pending' WHERE j.status = 'pending'
{}
ORDER BY j.created_at ASC ORDER BY j.created_at ASC
LIMIT ?1", LIMIT ?1",
)?; folder_exclusion_clause("i", paused_folder_ids)
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt let rows = stmt
.query_map([limit as i64], |row| { .query_map([limit as i64], |row| {
Ok(TaggingJob { Ok(TaggingJob {
@@ -1695,10 +1708,7 @@ fn get_pending_tagging_jobs_excluding(
}) })
})? })?
.collect::<rusqlite::Result<Vec<_>>>()?; .collect::<rusqlite::Result<Vec<_>>>()?;
Ok(rows Ok(rows)
.into_iter()
.filter(|job| !paused_folder_ids.contains(&job.folder_id))
.collect())
} }
pub fn update_ai_tags( pub fn update_ai_tags(
+12 -10
View File
@@ -365,8 +365,10 @@ const PAGE_SIZE = 200;
const AI_CAPTIONS_ENABLED_KEY = "phokus.aiCaptionsEnabled"; const AI_CAPTIONS_ENABLED_KEY = "phokus.aiCaptionsEnabled";
const SIMILAR_DISTANCE_THRESHOLD = 0.24; const SIMILAR_DISTANCE_THRESHOLD = 0.24;
// Single token shared by all gallery-producing requests (folder loads, searches,
// similarity, region search). Any new request increments it so a stale response
// from a previous collection type cannot overwrite newer results.
let galleryRequestToken = 0; let galleryRequestToken = 0;
let similarRequestToken = 0;
let tagCloudRequestToken = 0; let tagCloudRequestToken = 0;
let exploreTagRequestToken = 0; let exploreTagRequestToken = 0;
@@ -902,7 +904,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}, },
showVisualCluster: async (imageIds) => { showVisualCluster: async (imageIds) => {
const requestToken = ++similarRequestToken; const requestToken = ++galleryRequestToken;
set((state) => ({ set((state) => ({
activeView: "gallery", activeView: "gallery",
search: "", search: "",
@@ -923,7 +925,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
const images = await invoke<ImageRecord[]>("get_images_by_ids", { const images = await invoke<ImageRecord[]>("get_images_by_ids", {
params: { image_ids: imageIds }, params: { image_ids: imageIds },
}); });
if (requestToken !== similarRequestToken) return; if (requestToken !== galleryRequestToken) return;
set({ set({
images, images,
totalImages: images.length, totalImages: images.length,
@@ -933,7 +935,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
collectionTitle: "Explore Cluster", collectionTitle: "Explore Cluster",
}); });
} catch (error) { } catch (error) {
if (requestToken !== similarRequestToken) return; if (requestToken !== galleryRequestToken) return;
set({ set({
images: [], images: [],
totalImages: 0, totalImages: 0,
@@ -951,7 +953,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}, },
loadSimilarImages: async (imageId, folderId = get().selectedFolderId, reset = true, sourceFolderId = folderId ?? null) => { loadSimilarImages: async (imageId, folderId = get().selectedFolderId, reset = true, sourceFolderId = folderId ?? null) => {
const requestToken = ++similarRequestToken; const requestToken = ++galleryRequestToken;
const offset = reset ? 0 : get().loadedCount; const offset = reset ? 0 : get().loadedCount;
const similarScope = folderId === null ? "all_media" : "current_folder"; const similarScope = folderId === null ? "all_media" : "current_folder";
set((state) => ({ set((state) => ({
@@ -978,7 +980,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}, },
}); });
if (requestToken !== similarRequestToken) return; if (requestToken !== galleryRequestToken) return;
set((state) => { set((state) => {
const nextImages = reset ? result.images : [...state.images, ...result.images]; const nextImages = reset ? result.images : [...state.images, ...result.images];
@@ -999,7 +1001,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}; };
}); });
} catch (error) { } catch (error) {
if (requestToken !== similarRequestToken) return; if (requestToken !== galleryRequestToken) return;
console.error("Failed to load similar images:", error); console.error("Failed to load similar images:", error);
set({ set({
images: [], images: [],
@@ -1019,7 +1021,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}, },
loadSimilarByRegion: async (imageId, crop, folderId = get().selectedFolderId, sourceFolderId = folderId ?? null) => { loadSimilarByRegion: async (imageId, crop, folderId = get().selectedFolderId, sourceFolderId = folderId ?? null) => {
const requestToken = ++similarRequestToken; const requestToken = ++galleryRequestToken;
const similarScope = folderId === null ? "all_media" : "current_folder"; const similarScope = folderId === null ? "all_media" : "current_folder";
set((state) => ({ set((state) => ({
images: [], images: [],
@@ -1050,7 +1052,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}, },
}); });
if (requestToken !== similarRequestToken) return; if (requestToken !== galleryRequestToken) return;
set({ set({
images: result.images, images: result.images,
@@ -1067,7 +1069,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
similarScope, similarScope,
}); });
} catch (error) { } catch (error) {
if (requestToken !== similarRequestToken) return; if (requestToken !== galleryRequestToken) return;
console.error("Failed to load region search results:", error); console.error("Failed to load region search results:", error);
set({ set({
images: [], images: [],