[object Object]

← back to Rentv

pr/jobs: Cody gate on convergence-controller — (1) singleton guard: the self-re-enqueue chain changes tick each hop so it doesn't dedupe on payload; a second chain (admin re-trigger over a waiting one) would poll in parallel → count convergence-controller jobs, stop('duplicate_controller') if >1. (2) pending correctness: the drain count included jobs in retry-backoff (run_after in the future) which aren't actually draining and could inflate the wait / false-trip pending_stuck → added AND run_after <= now(). Verified premises against schema (run_after NOT NULL, indexed)

af6ccef9d071ea41f55abd2850dba0989b07bef9 · 2026-08-05 23:25:23 -0700 · Steve

Files touched

Diff

commit af6ccef9d071ea41f55abd2850dba0989b07bef9
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 23:25:23 2026 -0700

    pr/jobs: Cody gate on convergence-controller — (1) singleton guard: the self-re-enqueue chain changes tick each hop so it doesn't dedupe on payload; a second chain (admin re-trigger over a waiting one) would poll in parallel → count convergence-controller jobs, stop('duplicate_controller') if >1. (2) pending correctness: the drain count included jobs in retry-backoff (run_after in the future) which aren't actually draining and could inflate the wait / false-trip pending_stuck → added AND run_after <= now(). Verified premises against schema (run_after NOT NULL, indexed)
---
 src/pr/jobs/index.js | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/pr/jobs/index.js b/src/pr/jobs/index.js
index b805f9b6..8c2e633d 100644
--- a/src/pr/jobs/index.js
+++ b/src/pr/jobs/index.js
@@ -515,7 +515,8 @@ const handlers = {
           AND at > now() - interval '20 minutes'`, [])).n);
     const pending = Number((await db.one(
       `SELECT count(*)::int AS n FROM pr_jobs WHERE status IN ('queued','running')
-        AND job_type IN ('discover-people-linkedin','verify-organization','discover-people')`, [])).n);
+        AND job_type IN ('discover-people-linkedin','verify-organization','discover-people')
+        AND run_after <= now()`, [])).n); // only jobs actually ready/running — a job in retry-backoff (run_after in the future) isn't "draining", so don't let it inflate the wait or trip pending_stuck
     const used = Number(await settings.get('search_queries_used', 0));
     const budget = Number(await settings.get('search_query_budget', 1000));
 
@@ -525,6 +526,12 @@ const handlers = {
       await audit.log({ actor: 'system:convergence-controller', action: 'convergence.stop', detail: stateDoc });
       return stateDoc;
     };
+    // Singleton guard: the self-re-enqueue chain changes `tick` each hop, so it does NOT dedupe on
+    // payload — a second chain (e.g. an admin re-trigger over a waiting one) would poll in parallel
+    // and double the re-enqueue cadence. If more than this controller is in flight, halt the extra.
+    const controllers = Number((await db.one(
+      `SELECT count(*)::int AS n FROM pr_jobs WHERE job_type='convergence-controller' AND status IN ('queued','running')`, [])).n);
+    if (controllers > 1) return stop('duplicate_controller');
     if (pending > 0) { // research still draining — check again shortly, same round
       // Bound the wait: a job wedged in queued/running would otherwise re-tick this controller
       // every 3 min forever (none of the round/budget caps are reached while we're in this branch).

← 7c58dbc9 pr/jobs: cap convergence-controller re-ticks (runaway guard)  ·  back to Rentv  ·  auto-save: 2026-08-05T23:46:47 (7 files) — data/deals-regist d6396bdc →