feat(queue): fase 2 compleet — zeven MCP-tools met de mis-routing-fix #96

Merged
janpeter merged 5 commits from feat/queue-tools-push into main 2026-07-26 03:31:04 +02:00
Owner

Taken 8 t/m 15 van het fase-2-plan: de zeven queue-tools, hun registratie, en de integratietests. Daarmee is fase 2 af.

Wat dit oplost

De s4m-queue adresseert op (server, model). Vandaag claimt inbox --as claude het oudste pending antwoord voor dat adres, ongeacht wie het verzoek stuurde — dus twee Claude-sessies op één host stelen elkaars antwoorden, en de bestolen sessie wacht voor niets. queue_wait_reply lost dat op met het correlatiefilter in_reply_to = ANY(message_ids) in de WHERE-clause: een sessie kan per constructie alleen antwoorden op haar eigen request-handles claimen.

De tools

queue_push insert met source='mcp', meta-validatie, repo-autofill
queue_wait_reply de mis-routing-fix, met idempotente herlezing en bounded wait
queue_next FIFO-claim met onvoorspelbaar claim-token
queue_done / queue_fail afronden, met de zevendelige eigenaarsmatrix
queue_status / queue_list read-only, niet-claimend

Geregistreerd stdio-only. De centrale HTTP-server kent de caller-identiteit niet en heeft geen lease-register; daar zouden claims aan het verkeerde adres hangen. Een test bewijst dat HTTP ze niet aanbiedt — mét positieve controle, want een negatieve assertie die niets meer toetst is erger dan geen.

Integratietests tegen een echte database

De unit-tests draaien op een gemockte Prisma-client, en die geeft zijn fixture terug ongeacht wat de query zegt. Vier defecten waren daardoor structureel onzichtbaar — alle vier overleefden ze de mutatietests en alle vier worden ze nu rood tegen scrum4me_test (volledige migratieset, mét de CHECK-constraints):

  1. Atomiciteit van doneWithReply. Reply-rij, statuswissel en beide NOTIFY's horen samen te committen. Splitsen in twee transacties kwam door élke unit-test heen. Aangetoond met een tijdelijke Postgres-trigger die middenin de transactie faalt: onder de correcte code rolt alles terug; met de mutatie blijft er een weesreply achter waarvan het verzoek op claimed hangt en nooit done wordt.
  2. De type-array van claimNextRequest. QUEUE_REQUEST_TYPES vervangen door QUEUE_RESPONSE_TYPES liet negen tests groen — queue_next zou antwoorden claimen in plaats van verzoeken.
  3. previous_status uit de pre-update rij. Uit de post-update rij lezen kwam overal doorheen; elke claim-envelope zou dan de nieuwe status als "vorige" dragen.
  4. Of het reclaim-interval de query bereikt. Een hardgecodeerde default maakte de env-override stil dood.

Plus de §8-scenario's zelf: de correlatie-race met antwoorden in omgekeerde volgorde, claim-atomiciteit onder parallellisme, de idempotente-read-voortgang en de volledige eigenaarsmatrix tegen echte rijen.

17 integratietests, elf keer gedraaid, geen flakiness. Zonder TEST_DATABASE_URL skippen ze netjes.

Wat de mutatietests opleverden

Over deze acht taken overleefden aanvankelijk tientallen mutaties. De belangrijkste:

Een autorisatie-bypass. Het claim-token vervangen door een constante kwam door alle zeven queue_next-tests heen. verifyLocalOwnership sleutelt de lease op bericht-id en vertrouwt het token als enige bewijs — en bericht-ids zijn zichtbaar via queue_list. Met een voorspelbaar token kan een aanroeper binnen hetzelfde proces queue_done of queue_fail uitvoeren op werk dat hij nooit claimde. Nu vastgepind met twee claims naast elkaar.

Een precedentieregel die het plan zelf load-bearing noemt — expliciete meta.task.cwd boven de cwd-parameter — had geen enkele test. De ontvanger zou zijn werk in de verkeerde map uitvoeren.

Een read-only-garantie die op toeval berustte. Dat een write in queue_status faalde, kwam doordat de mock geen update-methode had, niet doordat een assertie het verbood.

Verificatie

npm run typecheck schoon. npm test: 1306 groen, 18 overgeslagen (twee integratiebestanden zonder env), 177 bestanden.

Bekend, niet in deze PR

  • Niets in de suite start index.ts echt op, dus een verdwenen registerQueueTools(server) zou ongemerkt shippen — dezelfde blinde vlek bestond al voor registerWorktreeTools. Onderzocht: niet te sluiten zonder productiecode-wijziging, want main() doet auth en presence-registratie vóór de transport verbindt.
  • scripts/smoke-test.ts:63 pint 16 tools; dat waren er al 54 vóór deze PR en nu 61. Draait niet in CI.
  • Een pre-existing race in create_pbi's code-allocatie (withCodeUniqueRetry retryt zonder backoff) — apart gemeld, niet aangeraakt.

🤖 Generated with Claude Code

Taken **8 t/m 15** van het fase-2-plan: de zeven queue-tools, hun registratie, en de integratietests. Daarmee is fase 2 af. ## Wat dit oplost De s4m-queue adresseert op `(server, model)`. Vandaag claimt `inbox --as claude` het **oudste** pending antwoord voor dat adres, ongeacht wie het verzoek stuurde — dus twee Claude-sessies op één host stelen elkaars antwoorden, en de bestolen sessie wacht voor niets. `queue_wait_reply` lost dat op met het correlatiefilter `in_reply_to = ANY(message_ids)` **in de WHERE-clause**: een sessie kan per constructie alleen antwoorden op haar eigen request-handles claimen. ## De tools | | | |---|---| | `queue_push` | insert met `source='mcp'`, meta-validatie, repo-autofill | | `queue_wait_reply` | de mis-routing-fix, met idempotente herlezing en bounded wait | | `queue_next` | FIFO-claim met onvoorspelbaar claim-token | | `queue_done` / `queue_fail` | afronden, met de zevendelige eigenaarsmatrix | | `queue_status` / `queue_list` | read-only, niet-claimend | Geregistreerd **stdio-only**. De centrale HTTP-server kent de caller-identiteit niet en heeft geen lease-register; daar zouden claims aan het verkeerde adres hangen. Een test bewijst dat HTTP ze niet aanbiedt — mét positieve controle, want een negatieve assertie die niets meer toetst is erger dan geen. ## Integratietests tegen een echte database De unit-tests draaien op een gemockte Prisma-client, en die geeft zijn fixture terug ongeacht wat de query zegt. Vier defecten waren daardoor **structureel onzichtbaar** — alle vier overleefden ze de mutatietests en alle vier worden ze nu rood tegen `scrum4me_test` (volledige migratieset, mét de CHECK-constraints): 1. **Atomiciteit van `doneWithReply`.** Reply-rij, statuswissel en beide NOTIFY's horen samen te committen. Splitsen in twee transacties kwam door élke unit-test heen. Aangetoond met een tijdelijke Postgres-trigger die middenin de transactie faalt: onder de correcte code rolt alles terug; met de mutatie blijft er een **weesreply** achter waarvan het verzoek op `claimed` hangt en nooit `done` wordt. 2. **De type-array van `claimNextRequest`.** `QUEUE_REQUEST_TYPES` vervangen door `QUEUE_RESPONSE_TYPES` liet negen tests groen — `queue_next` zou antwoorden claimen in plaats van verzoeken. 3. **`previous_status` uit de pre-update rij.** Uit de post-update rij lezen kwam overal doorheen; elke claim-envelope zou dan de nieuwe status als "vorige" dragen. 4. **Of het reclaim-interval de query bereikt.** Een hardgecodeerde default maakte de env-override stil dood. Plus de §8-scenario's zelf: de correlatie-race met antwoorden in omgekeerde volgorde, claim-atomiciteit onder parallellisme, de idempotente-read-voortgang en de volledige eigenaarsmatrix tegen echte rijen. **17 integratietests, elf keer gedraaid, geen flakiness.** Zonder `TEST_DATABASE_URL` skippen ze netjes. ## Wat de mutatietests opleverden Over deze acht taken overleefden aanvankelijk tientallen mutaties. De belangrijkste: **Een autorisatie-bypass.** Het claim-token vervangen door een constante kwam door alle zeven `queue_next`-tests heen. `verifyLocalOwnership` sleutelt de lease op bericht-id en vertrouwt het token als enige bewijs — en bericht-ids zijn zichtbaar via `queue_list`. Met een voorspelbaar token kan een aanroeper binnen hetzelfde proces `queue_done` of `queue_fail` uitvoeren op werk dat hij nooit claimde. Nu vastgepind met twee claims naast elkaar. **Een precedentieregel die het plan zelf load-bearing noemt** — expliciete `meta.task.cwd` boven de `cwd`-parameter — had geen enkele test. De ontvanger zou zijn werk in de verkeerde map uitvoeren. **Een read-only-garantie die op toeval berustte.** Dat een write in `queue_status` faalde, kwam doordat de mock geen `update`-methode had, niet doordat een assertie het verbood. ## Verificatie `npm run typecheck` schoon. `npm test`: **1306 groen**, 18 overgeslagen (twee integratiebestanden zonder env), 177 bestanden. ## Bekend, niet in deze PR - Niets in de suite start `index.ts` echt op, dus een verdwenen `registerQueueTools(server)` zou ongemerkt shippen — dezelfde blinde vlek bestond al voor `registerWorktreeTools`. Onderzocht: niet te sluiten zonder productiecode-wijziging, want `main()` doet auth en presence-registratie vóór de transport verbindt. - `scripts/smoke-test.ts:63` pint 16 tools; dat waren er al 54 vóór deze PR en nu 61. Draait niet in CI. - Een pre-existing race in `create_pbi`'s code-allocatie (`withCodeUniqueRetry` retryt zonder backoff) — apart gemeld, niet aangeraakt. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Vier mutaties overleefden de elf tests uit het plan. De ernstigste raakt een
regel die het plan zelf load-bearing noemt: een expliciete meta.task.cwd hoort
te winnen van de cwd-parameter, en niets bewees dat -- omgekeerd voert de
ontvanger zijn werk in de verkeerde map uit.

Verder: elke test stubte S4M_SERVER=mac, dus een hardgecodeerd 'mac' in de
insert bleef onzichtbaar; meta mocht verdwijnen bij info-berichten; en een
optimistische NOTIFY vóór de insert met een verzonnen envelope kwam er
ongezien doorheen. Die laatste is nu vastgepind via invocationCallOrder plus
een id-vergelijking met de opgeslagen rij.

Toegevoegd omdat mutatie A het naburige gat blootlegde: niets bewees dat een
mislukte notify de tool niet meesleept, terwijl dat het contract is.

Geen productiecode gewijzigd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test(queue): maak read-only een assertie en dek de include_terminal-default
All checks were successful
CI / Verify (pull_request) Successful in 1m50s
22d37fb49a
De read-only-garantie van queue_status werd alleen per ongeluk beschermd: een
toegevoegde write faalde omdat de mock geen update-methode definieerde, niet
omdat een assertie het verbood. Komt er later een rijkere gedeelde Prisma-mock,
dan verdwijnt die bescherming geruisloos -- terwijl niet-muteren juist het
definierende kenmerk is: wie pollt of er antwoord is mag dat antwoord niet
consumeren.

De schrijfmethodes staan nu in de mock en worden expliciet niet-aangeroepen
verklaard.

Daarnaast was de include_terminal-default op geen van beide niveaus gedekt,
omdat elke aanroep de parameter expliciet meegaf en de harnas Zod overslaat.

Geen productiecode gewijzigd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(runbook): het vangnet is niet meer compleet, en waarom
All checks were successful
CI / Verify (pull_request) Successful in 1m51s
1b7d359437
Gemeten op 2026-07-26: ops_dashboard.agent_message ging van 330 naar 51 rijen
en de nieuwe scrum4me.agent_message van 332 naar 8, doordat JP de
retentie-purge uit het Messages-dashboard draaide. Geen incident -- dat is de
purge die zijn werk doet -- maar het runbook beweerde nog dat de oude tabel
onaangeroerd bleef, en daar mag niemand meer op rekenen. Het archief is leeg:
de purge verwijdert zonder te archiveren, anders dan cleanup.

Leerpunt vastgelegd: de env-wijziging en de container-herstart zijn twee
momenten. Dat ook de oude database gekrompen is, betekent dat een purge draaide
toen de workers-container zijn nieuwe OPS_DATABASE_URL nog niet had ingelezen
-- het dashboard wees toen naar de oude DB terwijl de CLI's al om waren. Hier
onschadelijk omdat het een delete was en geen write, maar het venster bestaat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
janpeter changed title from feat(queue): queue_push, queue_status en queue_list to feat(queue): fase 2 compleet — zeven MCP-tools met de mis-routing-fix 2026-07-26 06:03:08 +02:00
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!96
No description provided.