fix(dispatch): sluit nooit gestarte pogingen van een afgemelde incarnatie (ISS-12) #191

Merged
janpeter merged 2 commits from fix/iss12-signed-off-unstarted into main 2026-10-05 12:21:40 +02:00
Owner

Fix voor ISS-12 (Forgejo #189). Na een OOM-herstart van de supervisor bleef de geclaimde poging als wees achter.

Faalpad (2026-10-05)

  1. Om 00:43:50Z claimt de supervisor poging 79314efd.
  2. Om 00:43:51Z volgt een OOM-kill tijdens get_artifact, nog vóór er een scope bestond.
  3. De herstarte supervisor registreert een nieuwe incarnatie. De oude incarnatie wordt afgemeld.
  4. Om 00:45:53Z komt lease_expired en gaat het request naar UNCERTAIN. claude_jobs blijft echter op CLAIMED staan en de reservering blijft bezet.
  5. De cancel van 01:07Z blijft op stop_required hangen tot handmatige operator_recovery.

Waar de fix hoort: de service

De supervisor (scrum4me-docker) erft na een herstart bewust niets. Hij heeft geen journal-scan, en de AttemptProof staat alleen in het geheugen. Bovendien eist claim-stop request-state CLAIMED. De supervisor kan de wees dus niet sluiten.

Fix

De nieuwe tick-stap closeSignedOffUnstartedAttempts (src/dispatch/orphans.ts) sluit pogingen met deze signatuur:

  • de incarnatie is afgemeld (signed_off_at)
  • scope_id en started_at zijn leeg
  • er is geen started_scope
  • er is nog geen stop, resultaat of openstaande publicatie

De stap legt eerst stopbewijs vast (stop_accepted, kind signed_off_unstarted). Daarna:

  • CLAIMED of UNCERTAIN wordt FAILED met summary DISPATCH_INCARNATION_SIGNED_OFF_BEFORE_START;
  • CANCEL_REQUESTED wordt CANCELLED.

In beide gevallen worden job en attempt terminaal en komt de reservering vrij. De signatuur wordt onder de lifecycle-locks opnieuw gecontroleerd.

Waarom dit veilig is

  • signPermit wordt alleen aangeroepen in startDispatchAttempt, in dezelfde transactie die started_at/scope_id zet en started_scope emit.
  • De broker weigert elke start zonder geldig permit (verifyBrokerPermit).
  • active() weigert een afgemelde incarnatie, dus die krijgt nooit meer een permit.

Er heeft dus niets gedraaid en er kan niets meer gaan draaien. Hooguit blijft een created, nooit gestarte container achter. Een poging van een nog actieve incarnatie houdt het bestaande gedrag: na lease-verloop wordt die UNCERTAIN en vraagt operator-herstel.

Tests

  • f9e7b06 voegt alleen de regressietest toe en is rood op origin/main. De incidentvolgorde blijft UNCERTAIN en de cancel blijft CANCEL_REQUESTED.
  • 2167725 is groen.
    • attempts.integration.test.ts: 30/30
    • volledige npm run test:dispatch: 295/295 (lokale wegwerp-pg17, schema-pin 6dc581da)
    • npm test: 2103 passed, 69 skipped
    • typecheck en typecheck:tests zijn schoon.
  • Discriminatiecheck: met de sign-off-voorwaarde uitgezet faalt precies de negatieve test "live incarnation".

Runbook: Scrum4Me PR docs/iss12-signed-off-unstarted (§9.1/9.2).

Niet mergen of deployen zonder aparte GO van JP.

🤖 Generated with Claude Code

Fix voor **ISS-12** (Forgejo #189). Na een OOM-herstart van de supervisor bleef de geclaimde poging als wees achter. ## Faalpad (2026-10-05) 1. Om 00:43:50Z claimt de supervisor poging `79314efd`. 2. Om 00:43:51Z volgt een OOM-kill tijdens `get_artifact`, nog vóór er een scope bestond. 3. De herstarte supervisor registreert een nieuwe incarnatie. De oude incarnatie wordt afgemeld. 4. Om 00:45:53Z komt `lease_expired` en gaat het request naar `UNCERTAIN`. `claude_jobs` blijft echter op `CLAIMED` staan en de reservering blijft bezet. 5. De cancel van 01:07Z blijft op `stop_required` hangen tot handmatige `operator_recovery`. ## Waar de fix hoort: de service De supervisor (scrum4me-docker) erft na een herstart bewust niets. Hij heeft geen journal-scan, en de AttemptProof staat alleen in het geheugen. Bovendien eist claim-stop request-state `CLAIMED`. De supervisor kan de wees dus niet sluiten. ## Fix De nieuwe tick-stap `closeSignedOffUnstartedAttempts` (`src/dispatch/orphans.ts`) sluit pogingen met deze signatuur: - de incarnatie is afgemeld (`signed_off_at`) - `scope_id` en `started_at` zijn leeg - er is geen `started_scope` - er is nog geen stop, resultaat of openstaande publicatie De stap legt eerst stopbewijs vast (`stop_accepted`, kind `signed_off_unstarted`). Daarna: - `CLAIMED` of `UNCERTAIN` wordt `FAILED` met summary `DISPATCH_INCARNATION_SIGNED_OFF_BEFORE_START`; - `CANCEL_REQUESTED` wordt `CANCELLED`. In beide gevallen worden job en attempt terminaal en komt de reservering vrij. De signatuur wordt onder de lifecycle-locks opnieuw gecontroleerd. **Waarom dit veilig is** - `signPermit` wordt alleen aangeroepen in `startDispatchAttempt`, in dezelfde transactie die `started_at`/`scope_id` zet en `started_scope` emit. - De broker weigert elke start zonder geldig permit (`verifyBrokerPermit`). - `active()` weigert een afgemelde incarnatie, dus die krijgt nooit meer een permit. Er heeft dus niets gedraaid en er kan niets meer gaan draaien. Hooguit blijft een *created*, nooit gestarte container achter. Een poging van een nog actieve incarnatie houdt het bestaande gedrag: na lease-verloop wordt die `UNCERTAIN` en vraagt operator-herstel. ## Tests - `f9e7b06` voegt alleen de regressietest toe en is **rood** op origin/main. De incidentvolgorde blijft `UNCERTAIN` en de cancel blijft `CANCEL_REQUESTED`. - `2167725` is **groen**. - `attempts.integration.test.ts`: 30/30 - volledige `npm run test:dispatch`: 295/295 (lokale wegwerp-pg17, schema-pin `6dc581da`) - `npm test`: 2103 passed, 69 skipped - `typecheck` en `typecheck:tests` zijn schoon. - Discriminatiecheck: met de sign-off-voorwaarde uitgezet faalt precies de negatieve test "live incarnation". Runbook: Scrum4Me PR `docs/iss12-signed-off-unstarted` (§9.1/9.2). **Niet mergen of deployen zonder aparte GO van JP.** 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reproduceert het faalpad van 2026-10-05. Een supervisor claimt, wordt
vervangen (nieuwe registratie meldt de oude incarnatie af) vóór de start, en
daarna blijft de poging CLAIMED/UNCERTAIN of hangt een cancel op
CANCEL_REQUESTED, met de reservering bezet. Negatief: een poging van een
actieve incarnatie en een gestarte poging blijven onaangeroerd.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(dispatch): sluit nooit gestarte pogingen van een afgemelde incarnatie (ISS-12)
All checks were successful
CI / PR candidate (never published) (pull_request) Successful in 5m22s
CI / Final merge attestation and immutable publication (pull_request) Has been skipped
2167725c45
Een supervisor die na zijn claim maar vóór de start wegvalt (OOM) en opnieuw
registreert, liet de poging als wees achter tot een operator ingreep.

De sweep in de tick sluit nu een poging met deze signatuur:
- de incarnatie is afgemeld
- scope_id en started_at zijn leeg
- er is geen started_scope-event

Hij legt eerst stopbewijs vast (kind signed_off_unstarted). Daarna:
- CLAIMED of UNCERTAIN wordt FAILED (DISPATCH_INCARNATION_SIGNED_OFF_BEFORE_START)
- CANCEL_REQUESTED wordt CANCELLED

In beide gevallen wordt de job terminaal en komt de reservering vrij.

Dit is veilig omdat het startpermit alleen wordt ondertekend in de transactie
die started_at zet, de broker geen start zonder permit accepteert, en active()
een afgemelde incarnatie nooit meer een permit geeft.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
s4m-codex-reviewer left a comment

COMMENT

Geen blokkerende codefinding gevonden; wel een kleine onnauwkeurigheid en beperkte onafhankelijke verificatie.

  • LOW — src/dispatch/orphans.ts:19: finishStoppedCancellation schrijft via src/dispatch/lifecycle.ts:60 dat de supervisor beëindiging bevestigde. In dit nieuwe pad leidt de service zelf het bewijs af en heeft de supervisor niets bevestigd. Gebruik een passende of neutrale rapporttekst, zodat het resultaat dezelfde herkomst beschrijft als het audit-event.

Plan gekoppeld via commits; references: ST-055. Er is niets omitted. De diff bevat de vijf functionele scenario’s uit de acceptatiecriteria, inclusief UNCERTAIN, annulering, negatieve controles en herhaalde tick. De afsluiting hergebruikt lifecycle-locks, resultaatregistratie en vrijgave van reserveringen. De runbookwijziging (§9.1/9.2, T-174) valt buiten deze diff en wordt in de PR expliciet als aparte Scrum4Me-PR genoemd; daarom is dat hier geen blokkerende finding. De inhoud van die aparte wijziging is niet geverifieerd.

Verificatie: head SHA komt overeen met de payload; git diff --check is schoon. Forgejo rapporteert een geslaagde PR-candidate CI voor deze SHA. De rood-op-main/groen-met-fix-resultaten staan in de PR-beschrijving; ik heb ze niet zelfstandig gereproduceerd. Lokaal strandde npm ci op esbuild EACCES; installatie zonder scripts slaagde, maar beide typechecks missen daardoor de gegenereerde Prisma Client. De dispatch-testguard weigert uitvoering wegens ontbrekende DISPATCH_TEST_SCHEMA_ROOT en er is geen lokale PostgreSQL-testomgeving. Deze omgevingsbeperkingen zijn geen aangetoonde PR-regressie, maar rechtvaardigen hier het voorzichtige COMMENT-verdict.

Bij de laatste controle was de PR inmiddels gemerged; deze review betreft uitsluitend commit 2167725c456e1e1a5a02442928568a90625bd0fb.

# COMMENT Geen blokkerende codefinding gevonden; wel een kleine onnauwkeurigheid en beperkte onafhankelijke verificatie. - **LOW — src/dispatch/orphans.ts:19:** `finishStoppedCancellation` schrijft via `src/dispatch/lifecycle.ts:60` dat de supervisor beëindiging bevestigde. In dit nieuwe pad leidt de service zelf het bewijs af en heeft de supervisor niets bevestigd. Gebruik een passende of neutrale rapporttekst, zodat het resultaat dezelfde herkomst beschrijft als het audit-event. Plan gekoppeld via commits; references: **ST-055**. Er is niets omitted. De diff bevat de vijf functionele scenario’s uit de acceptatiecriteria, inclusief UNCERTAIN, annulering, negatieve controles en herhaalde tick. De afsluiting hergebruikt lifecycle-locks, resultaatregistratie en vrijgave van reserveringen. De runbookwijziging (§9.1/9.2, T-174) valt buiten deze diff en wordt in de PR expliciet als aparte Scrum4Me-PR genoemd; daarom is dat hier geen blokkerende finding. De inhoud van die aparte wijziging is niet geverifieerd. Verificatie: head SHA komt overeen met de payload; `git diff --check` is schoon. Forgejo rapporteert een geslaagde PR-candidate CI voor deze SHA. De rood-op-main/groen-met-fix-resultaten staan in de PR-beschrijving; ik heb ze niet zelfstandig gereproduceerd. Lokaal strandde `npm ci` op esbuild EACCES; installatie zonder scripts slaagde, maar beide typechecks missen daardoor de gegenereerde Prisma Client. De dispatch-testguard weigert uitvoering wegens ontbrekende DISPATCH_TEST_SCHEMA_ROOT en er is geen lokale PostgreSQL-testomgeving. Deze omgevingsbeperkingen zijn geen aangetoonde PR-regressie, maar rechtvaardigen hier het voorzichtige COMMENT-verdict. Bij de laatste controle was de PR inmiddels gemerged; deze review betreft uitsluitend commit `2167725c456e1e1a5a02442928568a90625bd0fb`.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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!191
No description provided.