research: auto-transition processing → reviewing on last run

Hooks the topology_worker's post-terminal path into a new
notify_run_completed repo helper that atomically transitions the topic
processing → reviewing when the completed run has research_topic_id set
AND no siblings for that topic are still queued or running. Guarded on
status='processing' so a retry, a re-fire, or a topic already past
processing are all no-ops. Best-effort at the worker; DB hiccups are
logged and never fail the run.

The manual /submit-review endpoint stays as an escape hatch for topics
that end up parked in processing with nothing to complete (updated the
doc comment).
This commit is contained in:
Omar Sobh
2026-07-06 12:32:18 -07:00
parent 6d1dda6197
commit 8a4e222aec
6 changed files with 248 additions and 26 deletions
+31
View File
@@ -187,6 +187,37 @@ pub async fn touch(pool: &PgPool, id: Uuid) -> Result<(), DbError> {
Ok(())
}
/// If this run belongs to a research topic AND no siblings of that topic
/// are still queued or running, transition the topic `processing → reviewing`.
/// Guarded by `status = 'processing'` so a repeat call (e.g. a retry) is a
/// no-op; a topic already reviewing/publishing/published stays put.
/// Returns `true` when the topic was transitioned.
pub async fn notify_run_completed(pool: &PgPool, id: Uuid) -> Result<bool, DbError> {
// One statement: subquery locates the topic id, subquery counts siblings
// still in flight (excluding *this* run — it's about to be flipped to
// completed/failed by the caller, but ordering isn't guaranteed here).
let row = sqlx::query!(
"UPDATE research_topics t
SET status = 'reviewing', updated_at = now()
WHERE t.id = (
SELECT research_topic_id FROM topology_runs
WHERE id = $1 AND research_topic_id IS NOT NULL
)
AND t.status = 'processing'
AND NOT EXISTS (
SELECT 1 FROM topology_runs
WHERE research_topic_id = t.id
AND id <> $1
AND status IN ('queued', 'running')
)
RETURNING t.id",
id,
)
.fetch_optional(pool)
.await?;
Ok(row.is_some())
}
/// Mark a job completed and store its final result blob.
pub async fn complete(pool: &PgPool, id: Uuid, result: &Value) -> Result<(), DbError> {
sqlx::query!(