From eb6c0f86c4b4a769d0f95d4b18ac409721ffbfc8 Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Wed, 2 Sep 2026 17:02:19 -0500 Subject: [PATCH] fix(intake): wake the worker when a draft is regenerated (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/src/routes/adminItemDrafts.ts | 10 ++++++++++ backend/src/routes/intakeActions.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/backend/src/routes/adminItemDrafts.ts b/backend/src/routes/adminItemDrafts.ts index 833c4a2..4954413 100644 --- a/backend/src/routes/adminItemDrafts.ts +++ b/backend/src/routes/adminItemDrafts.ts @@ -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' }); }) ); diff --git a/backend/src/routes/intakeActions.ts b/backend/src/routes/intakeActions.ts index 7e60867..b6e5be8 100644 --- a/backend/src/routes/intakeActions.ts +++ b/backend/src/routes/intakeActions.ts @@ -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' }); }