feat(db): agent_message + agent_message_archive (s4m-queue fase 1) #136

Merged
janpeter merged 7 commits from feat/s4m-queue-tables into main 2026-07-16 13:35:24 +02:00
Owner

Fase 1c van de s4m-queue-migratie naar de scrum4me-DB. Scrum4Me is de designated migrator, dus de SQL-migratie landt hier. Spec + plan: scrum4me-mcp docs/superpowers/{specs,plans}/2026-07-12-s4m-queue-*.

Volgt op de gemergde s4m-queue #12 (cleanup-fix) en scrum4me-shared #37 (de modellen).

Wat er in zit

Commit
9d618d0 submodule-bump + geregenereerd schema + de migratie
4a81943 docs: herstelpad + cross-repo padverwijzingen
2d21548 test: guard DDL-identiteit en beide afwijkingen
68d6524 docs: herstelpad mag niet droppen zonder de fout te kennen (+ P3009)
22ff995 test: pin de kolomset van beide tabellen + precies twee indexen
04c514c docs: stap-3-guard dekt beide tabellen
209d69f test: pin meta's jsonb-default

De DDL is byte-identiek sinds 9d618d0 — md5 van het comment-gestripte bestand is gelijk over alle zeven commits (8a5dc5ab…). Alles daarna is comment en test.

DDL-equivalentie: het hele contract

De s4m-queue-CLI (raw SQL) en het Messages-dashboard blijven op deze tabellen werken en wisselen straks alléén van connection string. De DDL moet dus identiek zijn aan s4m-queue/migrations/001_init.sql + 002_archive.sql, op twee bewuste afwijkingen na.

Drie diffs tegen een echte Postgres, onafhankelijk gedraaid door implementer én reviewer (en nogmaals ná de comment-edits):

Vergelijking Uitkomst
Kolommen (information_schema.columns incl. ordinal_position) base 34, new 34 — diff leeg
Constraints (pg_constraint, schema-genormaliseerd) 7 vs 7 — exact één verschil: source_check krijgt 'mcp'
Indexen (pg_indexes) 3 vs 4 — exact één extra: agent_message_in_reply_to_idx

Precies de twee bedoelde afwijkingen, geen derde. De constraint-dump bevestigt ook dat het archief alleen zijn pkey draagt — nul CHECKs, nul FK, geen index, conform 002.

Subtiliteit die de review opleverde: PG17 slaat DEFAULT now() en DEFAULT CURRENT_TIMESTAMP verschillend op in de catalog. Deze migratie gebruikt now() (zoals 001), Prisma emit CURRENT_TIMESTAMP, en dit is de enige handgeschreven DEFAULT now() in echte DDL in deze repo. Een migrate diff --from-empty vergelijkt Prisma met Prisma en is daar structureel blind voor; de --from-config-datasource-variant tegen een scratch-schema met de echte DDL bewees dat Prisma beide normaliseert — geen drift.

Bewuste keuze: kale CREATE TABLE, géén IF NOT EXISTS

IF NOT EXISTS zou stil slagen tegen een tabel van afwijkende vorm — en DDL-identiteit is het hele contract hier. Dan levert het de CLI op runtime een kapotte tabel. Luid falen is correct. (De 14 IF NOT EXISTS-migraties in deze repo zijn hotfix-reconciliaties, een ander probleem.)

Niet-transactioneel — lees de header vóór de deploy

Prisma wrapt migratiebestanden niet. Getest op PG 17.10 met echte migratiehistorie: faalt dit bij het láátste statement, dan blijft agent_message staan én houdt _prisma_migrations finished_at IS NULL — waarna élke volgende deploy stopt met P3009 (de falende deploy zelf meldt P3018; in CI is P3009 het enige dat de operator ooit ziet). Omdat Scrum4Me designated migrator is, blokkeert dat de migratiepijplijn van het hele platform.

BEGIN;/COMMIT; lost dat op, maar degradeert de CLI-fout tot "current transaction is aborted" en verbergt de echte 42P07. Bewust niet gedaan: de dominante faalmodus (tabellen bestaan al) faalt bij statement 1 met nul residu. Het herstelpad staat in de header.

Die runbook is twee keer bijgesteld na review, en dat was nodig. De eerste versie vernietigde data in zijn eigen dominante faalmodus — gereproduceerd met 42 echte rijen:

rijen VOOR: 42
deploy → relation "agent_message" already exists   ← de dominante faalmodus
stap 1  → marked as rolled back
stap 2  → DROP TABLE IF EXISTS ...
stap 3  → All migrations have been successfully applied.
rijen NA: 0

Stil verlies, met een succesmelding erachteraan. De runbook was geschreven voor "faalt halverwege" (leeg restant, droppen veilig) maar was het enige Herstel:-blok in het bestand. Nu is de drop conditioneel op de werkelijke fout en eist hij count(*) = 0 op beide tabellen. Beide scenario's zijn daarna letterlijk nagespeeld: 42 → 42, en het legitieme halve-fail-herstel blijft uitvoerbaar.

Guard-test

__tests__/db/agent-message-queue-migration.test.ts, string-assert-huisstijl conform de acht precedenten in __tests__/db/. De guard in scrum4me-shared bewaakt het Prisma-schema; deze bewaakt de migratie-SQL, waar het identiteitscontract en beide afwijkingen leven.

7 tests, elk aantoonbaar rood tegen een echte sabotage:

Sabotage
source-CHECK zonder 'mcp' rood
in_reply_to-index weg rood (2× — afwijking én telling)
CHECK gesmokkeld in het archief rood
derde, ongedeclareerde index rood
extra kolom (34 → 35) rood
kolom weg uit het archief rood
body verliest NOT NULL rood
meta-default '{}''[]' in één tabel rood

De laatste is per CREATE TABLE-blok geassert, niet over het hele bestand: beide tabellen dragen meta … DEFAULT '{}', dus een whole-file-assert blijft groen bij eenzijdige drift — precies wat hij moet vangen. Het mutatie-harnas breekt af als een vervanging niets raakt, dus geen vacuüm-rode tests.

Twee dingen die je moet weten

1. De submodule-bump landt shared PR #36 mee. resolveRuntimeJobConfig verandert CODEX-modelresolutie van k.codex_model ?? null naar isCodexModel(...) ? ... : 'gpt-5.5', en versmalt model: string | nullstring. Geen defect — main draagt de tegenhanger-datamigratie 20260714160000_set_codex_model_floor al, dus de bump sluit juist een achterstand, en de 43 geraakte tests slagen. Staat ook in de commit-body van 9d618d0.

2. Gitlink-conflict met chore/gen-schema-drop-url-strip. Die branch bumpt de submodule naar ab6e97b, dat niet op shared's main zit maar op een chore-branch daar. Deze branch pint op shared's main-tip. Geen van beide bevat de ander → conflict voor wie tweede merget. Suggestie: deze eerst, dan landt die branch zijn shared-PR en herpint.

Verificatie

  • npm test: 213 bestanden / 1654 tests, exit 0 (de ene skip is hierarchical-order-migration-live die zichzelf skipt zonder TEST_DATABASE_URL)
  • npx prisma validate: groen
  • CLI-compat (spec §8): s4m-queue-suite 12 bestanden / 74 tests groen — ook gedraaid tégen deze migratie-DDL, dus direct bewijs in plaats van transitief
  • Geen enkele actie tegen de productie-DB; test-DB schoon achtergelaten

De deploy naar productie is een aparte taak met expliciete hardstop.

🤖 Generated with Claude Code

Fase 1c van de s4m-queue-migratie naar de scrum4me-DB. Scrum4Me is de **designated migrator**, dus de SQL-migratie landt hier. Spec + plan: `scrum4me-mcp` `docs/superpowers/{specs,plans}/2026-07-12-s4m-queue-*`. Volgt op de gemergde [s4m-queue #12](https://git.jp-visser.nl/janpeter/s4m-queue/pulls/12) (cleanup-fix) en [scrum4me-shared #37](https://git.jp-visser.nl/janpeter/scrum4me-shared/pulls/37) (de modellen). ## Wat er in zit | Commit | | |---|---| | `9d618d0` | submodule-bump + geregenereerd schema + de migratie | | `4a81943` | docs: herstelpad + cross-repo padverwijzingen | | `2d21548` | test: guard DDL-identiteit en beide afwijkingen | | `68d6524` | docs: herstelpad mag niet droppen zonder de fout te kennen (+ P3009) | | `22ff995` | test: pin de kolomset van beide tabellen + precies twee indexen | | `04c514c` | docs: stap-3-guard dekt beide tabellen | | `209d69f` | test: pin meta's jsonb-default | **De DDL is byte-identiek sinds `9d618d0`** — md5 van het comment-gestripte bestand is gelijk over alle zeven commits (`8a5dc5ab…`). Alles daarna is comment en test. ## DDL-equivalentie: het hele contract De s4m-queue-CLI (raw SQL) en het Messages-dashboard blijven op deze tabellen werken en wisselen straks alléén van connection string. De DDL moet dus identiek zijn aan `s4m-queue/migrations/001_init.sql` + `002_archive.sql`, op twee bewuste afwijkingen na. Drie diffs tegen een echte Postgres, onafhankelijk gedraaid door implementer én reviewer (en nogmaals ná de comment-edits): | Vergelijking | Uitkomst | |---|---| | Kolommen (`information_schema.columns` incl. `ordinal_position`) | base 34, new 34 — **diff leeg** | | Constraints (`pg_constraint`, schema-genormaliseerd) | 7 vs 7 — **exact één verschil**: `source_check` krijgt `'mcp'` | | Indexen (`pg_indexes`) | 3 vs 4 — **exact één extra**: `agent_message_in_reply_to_idx` | Precies de twee bedoelde afwijkingen, geen derde. De constraint-dump bevestigt ook dat het archief alleen zijn pkey draagt — nul CHECKs, nul FK, geen index, conform 002. **Subtiliteit die de review opleverde:** PG17 slaat `DEFAULT now()` en `DEFAULT CURRENT_TIMESTAMP` verschillend op in de catalog. Deze migratie gebruikt `now()` (zoals 001), Prisma emit `CURRENT_TIMESTAMP`, en dit is de enige handgeschreven `DEFAULT now()` in echte DDL in deze repo. Een `migrate diff --from-empty` vergelijkt Prisma met Prisma en is daar structureel blind voor; de `--from-config-datasource`-variant tegen een scratch-schema met de echte DDL bewees dat Prisma beide normaliseert — geen drift. ## Bewuste keuze: kale `CREATE TABLE`, géén `IF NOT EXISTS` `IF NOT EXISTS` zou stil slagen tegen een tabel van afwijkende vorm — en DDL-identiteit is het hele contract hier. Dan levert het de CLI op runtime een kapotte tabel. Luid falen is correct. (De 14 `IF NOT EXISTS`-migraties in deze repo zijn hotfix-reconciliaties, een ander probleem.) ## Niet-transactioneel — lees de header vóór de deploy Prisma wrapt migratiebestanden niet. Getest op PG 17.10 met echte migratiehistorie: faalt dit bij het láátste statement, dan blijft `agent_message` staan én houdt `_prisma_migrations` `finished_at IS NULL` — waarna élke volgende deploy stopt met **P3009** (de falende deploy zelf meldt P3018; in CI is P3009 het enige dat de operator ooit ziet). Omdat Scrum4Me designated migrator is, blokkeert dat de migratiepijplijn van het hele platform. `BEGIN;`/`COMMIT;` lost dat op, maar degradeert de CLI-fout tot "current transaction is aborted" en verbergt de echte `42P07`. Bewust niet gedaan: de dominante faalmodus (tabellen bestaan al) faalt bij statement 1 met nul residu. Het herstelpad staat in de header. **Die runbook is twee keer bijgesteld na review, en dat was nodig.** De eerste versie vernietigde data in zijn eigen dominante faalmodus — gereproduceerd met 42 echte rijen: ``` rijen VOOR: 42 deploy → relation "agent_message" already exists ← de dominante faalmodus stap 1 → marked as rolled back stap 2 → DROP TABLE IF EXISTS ... stap 3 → All migrations have been successfully applied. rijen NA: 0 ``` Stil verlies, met een succesmelding erachteraan. De runbook was geschreven voor "faalt halverwege" (leeg restant, droppen veilig) maar was het enige `Herstel:`-blok in het bestand. Nu is de drop conditioneel op de werkelijke fout en eist hij `count(*) = 0` op **beide** tabellen. Beide scenario's zijn daarna letterlijk nagespeeld: 42 → 42, en het legitieme halve-fail-herstel blijft uitvoerbaar. ## Guard-test `__tests__/db/agent-message-queue-migration.test.ts`, string-assert-huisstijl conform de acht precedenten in `__tests__/db/`. De guard in scrum4me-shared bewaakt het Prisma-schema; deze bewaakt de migratie-SQL, waar het identiteitscontract en beide afwijkingen leven. 7 tests, elk aantoonbaar rood tegen een echte sabotage: | Sabotage | | |---|---| | source-CHECK zonder `'mcp'` | rood | | `in_reply_to`-index weg | rood (2× — afwijking én telling) | | CHECK gesmokkeld in het archief | rood | | derde, ongedeclareerde index | rood | | extra kolom (34 → 35) | rood | | kolom weg uit het archief | rood | | `body` verliest `NOT NULL` | rood | | `meta`-default `'{}'` → `'[]'` in één tabel | rood | De laatste is per `CREATE TABLE`-blok geassert, niet over het hele bestand: beide tabellen dragen `meta … DEFAULT '{}'`, dus een whole-file-assert blijft groen bij eenzijdige drift — precies wat hij moet vangen. Het mutatie-harnas breekt af als een vervanging niets raakt, dus geen vacuüm-rode tests. ## Twee dingen die je moet weten **1. De submodule-bump landt shared PR #36 mee.** `resolveRuntimeJobConfig` verandert CODEX-modelresolutie van `k.codex_model ?? null` naar `isCodexModel(...) ? ... : 'gpt-5.5'`, en versmalt `model: string | null` → `string`. Geen defect — main draagt de tegenhanger-datamigratie `20260714160000_set_codex_model_floor` al, dus de bump sluit juist een achterstand, en de 43 geraakte tests slagen. Staat ook in de commit-body van `9d618d0`. **2. Gitlink-conflict met `chore/gen-schema-drop-url-strip`.** Die branch bumpt de submodule naar `ab6e97b`, dat niet op shared's main zit maar op een chore-branch daar. Deze branch pint op shared's main-tip. Geen van beide bevat de ander → conflict voor wie tweede merget. Suggestie: deze eerst, dan landt die branch zijn shared-PR en herpint. ## Verificatie - `npm test`: 213 bestanden / 1654 tests, exit 0 (de ene skip is `hierarchical-order-migration-live` die zichzelf skipt zonder `TEST_DATABASE_URL`) - `npx prisma validate`: groen - CLI-compat (spec §8): s4m-queue-suite 12 bestanden / 74 tests groen — ook gedraaid tégen deze migratie-DDL, dus direct bewijs in plaats van transitief - Geen enkele actie tegen de productie-DB; test-DB schoon achtergelaten De deploy naar productie is een **aparte taak met expliciete hardstop**. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
De submodule-bump (258d4fc → cd19b4c) neemt naast de AgentMessage-modellen
ook shared PR #36 (codex-model-catalog) mee: resolveRuntimeJobConfig lost het
CODEX-model nu op als isCodexModel(k.codex_model) ? k.codex_model : 'gpt-5.5'
in plaats van k.codex_model ?? null, en versmalt model: string | null → string.
Geen defect: main heeft de tegenhanger-datamigratie
20260714160000_set_codex_model_floor al, dus de bump sluit een achterstand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prisma wrapt migratiebestanden niet in een transactie: een halve fail laat
agent_message staan en blokkeert via _prisma_migrations élke volgende deploy
(P3018). Scrum4Me is designated migrator, dus dat raakt het hele platform.
Herstelpad staat nu in de header, inclusief waarom er bewust geen BEGIN;/COMMIT;
omheen staat (diagnoseerbaarheid > atomiciteit bij de dominante faalmodus).

Spec- en 002_archive-verwijzingen zijn cross-repo geprefixt; de dangling
verwijzing naar "stap 4" is vervangen door de guard-test die het afdwingt.
DDL ongewijzigd — de drie diffs tegen 001+002 reproduceren exact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
De scrum4me-shared-guard bewaakt het Prisma-schema, niet de migratie-SQL —
terwijl het DDL-identiteitscontract met s4m-queue/migrations/001_init.sql +
002_archive.sql en beide bewuste afwijkingen juist daar leven. De CLI en het
Messages-dashboard blijven op deze tabellen draaien, dus stille drift breekt ze.

String-assert-huisstijl (codex-model-floor-migration.test.ts,
deploy-fields-migration.test.ts). Bewaakt: source-CHECK met 'mcp',
agent_message_in_reply_to_idx, en dat het archief-blok géén CHECK/FK/index/
default heeft — dat laatste op het DDL-blok, niet op het bestand, omdat de
comment-kop CHECK in proza noemt. Rood-bewijs: elk van de drie gesloopt op een
kopie laat precies één test falen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Critical: het herstelpad vernietigde data in zijn eigen dominante faalmodus.
Stap 2 dropte onvoorwaardelijk, maar bij 'relation "agent_message" already
exists' faalt statement 1 en heeft de migratie niets aangemaakt — dan dropt die
stap uitsluitend de berichten die er al stonden, waarna de volgende deploy
succes meldt. Op een live queue is dat stil dataverlies. Het blok was bedoeld
voor 'faalt halverwege', maar het is het enige Herstel:-blok in het bestand en
die scoping was impliciet. Stap 2 is nu conditioneel op de werkelijke fout en
stap 3 eist een count(*)-verificatie vóór de DROP.

Foutcode gecorrigeerd: alleen de falende deploy meldt P3018; élke volgende
stopt met P3009 (migrate found failed migrations in the target database) —
geverifieerd tegen Prisma's error-reference. Praktisch relevant omdat P3018
één keer voorbijflitst en P3009 in CI de enige code is die de operator ziet.

DDL onaangeroerd: md5 van het comment-gestripte bestand is 8a5dc5ab… net als
in 9d618d0; de drie diffs tegen 001+002 reproduceren exact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
De guard was removal-only: vier contract-brekende mutaties bleven groen — een
derde ongedeclareerde index, een extra kolom (34 -> 35), body dat NOT NULL
verliest, en een kolom weg uit het archief. Dat laatste is de scherpste: de
cleanup archiveert met INSERT ... SELECT over expliciete kolomlijsten, dus een
weggevallen archiefkolom breekt de queue pas op runtime.

Een toegepaste migratie is immutable, dus de verwachte kolomset hoort letterlijk
gepind — symmetrisch voor beide tabellen, met nullability in het type zoals de
Prisma-guard in scrum4me-shared het doet. Defaults blijven erbuiten: die
verschillen wél tussen de twee. De index-telling maakt 'exact twee afwijkingen,
niet meer' mechanisch.

Rood-bewijs: alle vier de mutaties falen nu, elk op precies één test; de drie
oorspronkelijke asserts blijven rood bij hun eigen sloop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
De count(*)-guard checkte alleen agent_message, terwijl de DROP eronder beide
tabellen raakt: bij een leeg agent_message náást een archief mét rijen staat de
guard eerlijk groen en gaan de archiefrijen alsnog dood. Dezelfde bugklasse als
de vorige fix, overlevend aan de kant die de check niet dekte. Stap 3 verifieert
nu beide tabellen (niet-bestaand telt als goed).

Stap 2 stuurde de operator naar 'zoek uit waaróm ze er al staan' zonder uitgang:
blijken de tabellen al correct, dan is deze migratie overbodig en vraagt dat om
resolve --applied — met --rolled-back loopt de retry op dezelfde fout vast.
Die halve regel staat er nu bij.

DDL onaangeroerd: md5 8a5dc5ab… gelijk aan 9d618d0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(db): pin meta's jsonb-default op '{}' in beide tabellen
All checks were successful
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 5m40s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
209d69fa17
Laatste default-gat: meta '{}' -> '[]' bleef groen. Bewust per CREATE
TABLE-blok geassert in plaats van op het hele bestand — beide tabellen dragen
dit default, dus een hele-bestand-toContain blijft groen zodra één van de twee
nog '{}' heeft. Rood-bewijs bevestigt dat: bij een mutatie in alleen
agent_message (of alleen het archief) staat de naïeve vorm vals groen en de
per-blok-vorm rood.

Titel van de archief-test gecorrigeerd: die claimde 'zonder kolom-defaults',
maar het archief draagt meta's '{}' wél — het mist alleen id/status/created_at.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

Verdict: APPROVED

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen blokkerende findings.

Review-notities

  • De migratie en Prisma-modellen volgen het gedocumenteerde agent_message/agent_message_archive contract: UUID/timestamptz/raw-SQL compatibiliteit, expliciete @@map, handmatige CHECK-constraints in SQL en bewuste afwijkingen voor source='mcp' en in_reply_to-index.
  • De toegevoegde migratietest pint de belangrijkste drift-risico's vast: kolomvolgorde, defaults, indexes, FK/CHECK-verschillen tussen live- en archieftabel.
  • De diff bevat een submodule-bump naar vendor/scrum4me-shared; de inhoud daarvan zit niet in de payload-diff en kon in deze review-runtime niet inhoudelijk worden geïnspecteerd. Op basis van de zichtbare diff is dat geen blocker.
# Verdict: APPROVED Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen blokkerende findings. ## Review-notities - De migratie en Prisma-modellen volgen het gedocumenteerde `agent_message`/`agent_message_archive` contract: UUID/timestamptz/raw-SQL compatibiliteit, expliciete `@@map`, handmatige CHECK-constraints in SQL en bewuste afwijkingen voor `source='mcp'` en `in_reply_to`-index. - De toegevoegde migratietest pint de belangrijkste drift-risico's vast: kolomvolgorde, defaults, indexes, FK/CHECK-verschillen tussen live- en archieftabel. - De diff bevat een submodule-bump naar `vendor/scrum4me-shared`; de inhoud daarvan zit niet in de payload-diff en kon in deze review-runtime niet inhoudelijk worden geïnspecteerd. Op basis van de zichtbare diff is dat geen blocker.
Sign in to join this conversation.
No reviewers
No labels
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!136
No description provided.