fix(intake): wake the worker when a draft is regenerated (#272)

Regenerate set state='queued', cleared attempts and ai_error, and answered 200 — then nothing ran the worker, so the row sat until the five-minute sweeper happened along. From the admin's side that is indistinguishable from a dead button, and the obvious response is to press it again.

The submission path has kicked the worker since #223. Both paths put a row into 'queued'; only one asked for it to be drafted. That was an oversight in #225 rather than a decision: the kick was added to the submission path in a later task and the action routes were never revisited.

Fixed in both places that re-queue — the admin route and the signed regenerate link from the notification email, which sets the identical state. Fire and forget with a logged catch, exactly as the submission path does: a slow or failing model call must not become a failed request for the admin, and the sweeper is still the backstop if the kick misses.

The card still does not update itself once the draft lands, since drafting takes a few seconds and the screen has no way to know it finished. That is a UI question rather than this bug, and is noted on #272.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 17:25:03 -05:00
co-authored by Claude Opus 5
parent 6c6aaa46eb
commit baa5bb4fdf
2 changed files with 20 additions and 0 deletions
+10
View File
@@ -1,6 +1,7 @@
import { Router, Request, Response } from 'express';
import { pool } from '../db';
import { asyncRoute } from '../asyncRoute';
import { draftQueued } from '../intake/draftingWorker';
import { nextPriceSource, PriceSource } from '../intake/priceSource';
const router = Router();
@@ -148,6 +149,15 @@ router.post(
[req.params.itemId]
);
if (rowCount === 0) return res.status(404).json({ error: 'no draft for this item' });
// Wake the worker rather than leaving the row for the five-minute sweeper.
// Both this and the submission path put a row into 'queued'; only that one
// asked for it to be drafted, which made this button indistinguishable from
// a dead one (#272). Fire and forget with a logged catch, exactly as there:
// a slow or failing model call must not become a failed request for the
// admin, and the sweeper is still the backstop if this misses.
void draftQueued(1).catch((err) => console.error('[drafting] after regenerate:', err));
res.json({ state: 'queued' });
})
);
+10
View File
@@ -1,6 +1,7 @@
import { Router, Request, Response } from 'express';
import { pool } from '../db';
import { asyncRoute } from '../asyncRoute';
import { draftQueued } from '../intake/draftingWorker';
import { IntakeAction, verifyAction } from '../intake/actionLinks';
const router = Router();
@@ -96,6 +97,15 @@ router.post(
[checked.itemId]
);
if (rowCount === 0) return res.status(404).json({ error: 'no draft for this item' });
// Wake the worker rather than leaving the row for the five-minute sweeper.
// Both this and the submission path put a row into 'queued'; only that one
// asked for it to be drafted, which made this button indistinguishable from
// a dead one (#272). Fire and forget with a logged catch, exactly as there:
// a slow or failing model call must not become a failed request for the
// admin, and the sweeper is still the backstop if this misses.
void draftQueued(1).catch((err) => console.error('[drafting] after regenerate:', err));
return res.json({ state: 'queued' });
}