fix(claim): tier-defer alleen naar peers die de job zelf kunnen claimen (tier-deadlock E2E) #66

Merged
janpeter merged 1 commit from fix/tier-defer-claimability into main 2026-07-04 19:30:00 +02:00
Owner

Bug (gevonden tijdens M17 fase-5 E2E)

buildHigherTierIdleFragment was capability-blind: een worker met een tier defereerde naar élke idle hogere-tier-worker van dezelfde user — ook als die de job nooit kan claimen. Op prod: de dedicated deploy-worker (per abuis WORKER_CAPABILITY=LOW_P geërfd uit worker-idea.env) defereerde eeuwig naar twee idle HIGH_P-workers op max2 zonder deploy-capability → de eerste "Deploy nu"-job bleef onclaimd QUEUED. Diagnose: s4m-taak 3bbb4d32 (dry-run met/zonder tier-fragment bewees het predicaat).

De directe unblock op de server was config (WORKER_CAPABILITY verwijderd uit worker-deploy.env); dit is de structurele fix zodat geen enkele toekomstige dedicated capability-worker hierop kan deadlocken.

Fix

Claimability-guard ín het NOT EXISTS: een hogere-tier idle peer telt alleen mee als hij de kandidaat-job (cj) zelf zou kunnen claimen. De twee takken spiegelen de claim-paden uit buildClaimableJobWhereFragment exact:

  • peer met caps exact [deploy] → telt alleen voor DEPLOY-jobs (deployOnly-pad) — dekt ook de spiegel-deadlock (deploy-peer die NULL-jobs zou reserveren);
  • overige peers → required_capability IS NULL OR = ANY(w.capabilities) (lege caps ⇒ alleen NULL-jobs).

Geen nieuwe bound values; Prisma.empty-bypass voor tier-loze workers ongewijzigd.

Verificatie

  • npx tsc --noEmit schoon; 908/908 tests groen (6 nieuwe regressietests).
  • Mutatie-toets: guard verwijderd → 3 tests rood; extra asserts pinnen de mutatie-overlevers uit de review (OR CASE/AND NOT CASE-connector, >>= tier-strictness).
  • Adversarieel 2-lenzen-panel: (1) SQL door de echte PG-parser (libpg_query) gehaald — CASE-precedentie klopt, OR lekt niet uit de ELSE-tak; NULL-waarheidstabel valt in álle combinaties de veilige kant op (peer telt niet mee ⇒ claimen, nooit deadlock); enum-literal-vergelijkingen hebben prod-precedent in ditzelfde bestand. (2) Gedragsmatrix cel-voor-cel: enige gedragswijzigingen zijn precies de gewenste; kritieke cel bevestigd: HIGH_P-peer met brede caps [deploy,review] blokkeert een deploy-job WEL (die kan hem via het generieke pad echt claimen).

Bekende restklasse (gedocumenteerd, niet gefixt)

Product-gescoped pollende peers: de guard kan per-call product_id niet spiegelen (wordt niet gepersisteerd in claude_workers). Dedicated workers horen unscoped te pollen — staat nu in de docstring.

Na merge

Zelfde rollout als #64: mcp-stable pull + runner-image rebake + recreate workers.

🤖 Generated with Claude Code

## Bug (gevonden tijdens M17 fase-5 E2E) `buildHigherTierIdleFragment` was capability-blind: een worker met een tier defereerde naar élke idle hogere-tier-worker van dezelfde user — ook als die de job nooit kan claimen. Op prod: de dedicated deploy-worker (per abuis `WORKER_CAPABILITY=LOW_P` geërfd uit worker-idea.env) defereerde eeuwig naar twee idle HIGH_P-workers op max2 zonder `deploy`-capability → de eerste "Deploy nu"-job bleef onclaimd QUEUED. Diagnose: s4m-taak `3bbb4d32` (dry-run met/zonder tier-fragment bewees het predicaat). De directe unblock op de server was config (`WORKER_CAPABILITY` verwijderd uit `worker-deploy.env`); dit is de structurele fix zodat geen enkele toekomstige dedicated capability-worker hierop kan deadlocken. ## Fix Claimability-guard ín het `NOT EXISTS`: een hogere-tier idle peer telt alleen mee als hij de kandidaat-job (`cj`) zelf zou kunnen claimen. De twee takken spiegelen de claim-paden uit `buildClaimableJobWhereFragment` exact: - peer met caps exact `[deploy]` → telt alleen voor `DEPLOY`-jobs (deployOnly-pad) — dekt ook de **spiegel-deadlock** (deploy-peer die NULL-jobs zou reserveren); - overige peers → `required_capability IS NULL OR = ANY(w.capabilities)` (lege caps ⇒ alleen NULL-jobs). Geen nieuwe bound values; `Prisma.empty`-bypass voor tier-loze workers ongewijzigd. ## Verificatie - `npx tsc --noEmit` schoon; **908/908** tests groen (6 nieuwe regressietests). - **Mutatie-toets:** guard verwijderd → 3 tests rood; extra asserts pinnen de mutatie-overlevers uit de review (`OR CASE`/`AND NOT CASE`-connector, `>` → `>=` tier-strictness). - **Adversarieel 2-lenzen-panel:** (1) SQL door de echte PG-parser (libpg_query) gehaald — CASE-precedentie klopt, OR lekt niet uit de ELSE-tak; NULL-waarheidstabel valt in álle combinaties de veilige kant op (peer telt niet mee ⇒ claimen, nooit deadlock); enum-literal-vergelijkingen hebben prod-precedent in ditzelfde bestand. (2) Gedragsmatrix cel-voor-cel: enige gedragswijzigingen zijn precies de gewenste; kritieke cel bevestigd: HIGH_P-peer met brede caps `[deploy,review]` blokkeert een deploy-job WEL (die kan hem via het generieke pad echt claimen). ## Bekende restklasse (gedocumenteerd, niet gefixt) Product-gescoped pollende peers: de guard kan per-call `product_id` niet spiegelen (wordt niet gepersisteerd in `claude_workers`). Dedicated workers horen unscoped te pollen — staat nu in de docstring. ## Na merge Zelfde rollout als #64: mcp-stable pull + runner-image rebake + recreate workers. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
M17-E2E-vondst (2026-07-04, diagnose s4m 3bbb4d32): buildHigherTierIdleFragment
was capability-blind — een dedicated deploy-worker met tier LOW_P defereerde
eeuwig naar idle HIGH_P-workers zonder 'deploy'-capability, waardoor de
MANUAL DEPLOY-job onclaimd QUEUED bleef (tier-deadlock). De guard correleert
de peer-claimbaarheid met de kandidaat-job en spiegelt beide claim-paden:
exact-['deploy']-peers tellen alleen mee voor DEPLOY-jobs (deployOnly-pad),
overige peers voor NULL-jobs of jobs waarvan required_capability in hun
capabilities zit — inclusief de spiegel-deadlock (deploy-peer die NULL-jobs
zou 'reserveren'). Geen nieuwe bound values; regressietests toegevoegd.

Directe unblock op de server was een config-fix (WORKER_CAPABILITY uit
worker-deploy.env); dit is de structurele reparatie.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/scrum4me-mcp!66
No description provided.