Model download and embed dispatches dedupe onto retained finished jobs (same add-dedupe behavior as #1213)

Author: caweisCreated Aug 6, 2026Updated Sep 13, 2026
Labelsreleased on @rc

While porting the fix from #1213 into my macOS fork I checked the other dispatch sites that pin a deterministic jobId. The behavior #1213 describes — queue.add with an existing custom jobId returns the existing job instead of throwing, and the client only throws on negative script results — reaches two more surfaces on dev. Verified against BullMQ 5's addStandardJob script; dev is on 5.77.6.

1. A completed model download blocks that model forever

DownloadModelJob.dispatch clears a previous job only when its state is failed (download_model_job.ts:168-175). Completed records are kept (removeOnComplete: false, line 185) and nothing else removes them; deleteModel in ollama_service.ts only calls the Ollama API.

Repro: download a model, let it complete, delete it in the UI, request it again. queue.add dedupes onto the retained completed job, dispatch returns created: true, the API replies "Download job dispatched for model: …", and no worker runs. Completed jobs aren't in the Downloads list (it fetches waiting/active/delayed/failed), so no Retry appears either. The model can't be reinstalled until Redis is flushed.

Extending the existing pre-clean from state === 'failed' to any non-live state fixes it.

2. Retrying a failed model download can delete the job it just dispatched

The model branch of retryFailedJob dispatches first and removes after (download_service.ts:194-195). Dispatch's pre-clean removes the failed record and enqueues a fresh waiting job under the same jobId; job.remove() on the old handle then removes by id, which is now the fresh job. If the worker locks it first, the remove throws and is swallowed, so whether the retry survives is a race. The file branch already does remove-then-dispatch (lines 205-207); the model branch needs the same order.

3. Index/retry of a file whose embed failed can no-op

An initial EmbedFileJob.dispatch without force pins the per-file jobId (embed_file_job.ts:388-390). Failed records are retained (removeOnFail: { count: 20 }), and the job already exists catch that would report the collision never fires, because add doesn't throw. So POST /api/rag/files/embed with force: false for a file whose earlier embed failed dedupes onto the retained failed record and returns 202 "Indexing queued for this file." with nothing enqueued. The record only leaves after 20 newer failures, which on a single-user box may be never. The force: true path is unaffected since it skips the deterministic id.

A pre-add check shaped like the model job's (return in-flight jobs, remove terminal ones before add) covers both this and bug 1, and makes the unreachable job already exists catches removable.

My fork's version of these fixes, in case the shape is useful: caweis/project-nomad@f21d1cc (model + embed pre-add cleanup, retry order flip) and caweis/project-nomad@80be398 (the #1213 fix applied to every RunDownloadJob guard). The fork's files have diverged, so they won't apply as patches, but the logic carries over.

Source: Crosstalk-Solutions/project-nomad