Scrum4Me/docs/patterns/claude-question-channel.md

6.6 KiB

title status audience language last_updated when_to_read
Bidirectionele async-comms MCP-agent ↔ user active
ai-agent
contributor
nl 2026-05-03 When implementing or extending the claude-question channel for agent-user async communication.

Patroon: Bidirectionele async-comms tussen MCP-agent en interactieve user

Het M11 vraag-antwoord-kanaal is herbruikbaar voor elke feature waarbij een autonome agent (Claude Code via MCP, een Vercel-job, etc.) iets wil ophelderen bij de actieve gebruiker zonder zelf te blokkeren of te raden.

"Agent stelt vraag → vraag wacht persistent → user beantwoordt op een moment dat het hem uitkomt → agent leest het antwoord en gaat door."

Voorbeelden waar dit zou kunnen passen:

  • AI-codereview: Claude vraagt om bevestiging op een controversiële refactor
  • Background-import: queue-worker vraagt om een keuze (overschrijven/skippen) per ambigue rij
  • Multi-step migratie: scripted task pauseert op een handmatige bevestiging
  • Approval-flow voor LLM-actions: user keurt een tool-call goed vóór 't echt gebeurt

Vier eindpunten

Endpoint Auth Doel
MCP ask_* (write) API-token + demo-blok maakt rij in *_questions-tabel; optioneel wait_seconds voor sync polling
MCP get_*_answer (read) API-token leest huidige status + antwoord (voor latere session-pickup)
POST /actions/answer (Server Action) iron-session + product-membership atomic updateMany WHERE status='open'
GET /api/realtime/<channel> (SSE) iron-session user-scoped stream, filter op entity + product-access

Plus:

  • Postgres-trigger op de tabel die pg_notify doet op een gedeeld channel met een {op, entity, id, ...}-payload
  • Cron-endpoint dat verlopen rijen markeert (status='expired') zodat achterstallige queries niet blijven groeien

Vier security-uitgangspunten

  1. Atomic state-transities. Antwoord-actie doet één UPDATE met alle invarianten in de WHERE (status, expiry, owner-check). Concurrent dubbele submit: PostgreSQL row-locking laat één caller count=1 zien, de rest 0.
  2. Demo-blok op writes. Agent-side via requireWriteAccess (PERMISSION_ DENIED voor demo-tokens), user-side via early-return op session.isDemo en disabled-submit-knop met tooltip in de UI.
  3. Access-isolation in SSE-filter. Bij connect: query alle accessible product-IDs voor deze user → Set. In notification-handler: drop alle payloads waarvan product_id ∉ accessibleSet. Dit is naast de DB-query- filter; redundant maar voorkomt lekkage als de DB-laag iets doorlaat dat niet zou moeten.
  4. Geen gevoelige data in logs. Payload bevat alleen IDs en status — de tekst van vraag en antwoord komt via een aparte authenticated query. console.log alleen question_id, nooit content.

Channel-strategie: hergebruik vs. eigen kanaal

Twee opties bij meerdere realtime-features:

Optie Voordeel Nadeel
Eigen channel per feature (M10 scrum4me_pairing) Geen filter-leakage tussen features 1 LISTEN-connectie per feature; meer DB-resources; meer routes
Gedeeld channel met entity-key (M11 scrum4me_changes) 1 LISTEN per route; nieuwe entity = filter-aanpassing Vergeten te filteren = leak

Voor M11 is gekozen voor hergebruik: één channel scaalt beter naar v2 (comments, mentions, status-updates allemaal op zelfde stream) en de filter- discipline is enforceable in code-review. Mitigatie voor leak-risico: expliciet if (payload.entity === 'X') return false in elke route die feature X niet hoort te zien — zoals app/api/realtime/solo/route.ts die entity:'question' weert.


TTL-richtlijn

  • Question lifetime: 24 u — kort genoeg dat verlaten queries niet ophopen, lang genoeg dat een gebruiker die afwezig is een nacht heeft om te antwoorden
  • MCP-tool wait_seconds: max 600 s — Claude wacht maximaal 10 min op een antwoord; daarna status: 'pending' zodat hij later kan terugkomen via get_question_answer
  • Cron schedule: 0 4 * * * — daily op een rustig tijdstip (Vercel Hobby staat alleen daily crons toe; Pro ondersteunt fijnmaziger). 24 u TTL + daily cleanup houdt de tabel klein zonder cron-budget te belasten

Sjabloon-bestanden

Specifiek voor M11. Kopieer en pas aan:

Database

  • prisma/schema.prisma: model met id, status, expires_at, denormalized product_id voor SSE-filter, asker/answerer-FKs, json options?-veld voor multiple-choice
  • prisma/migrations/<ts>/migration.sql: tabel-DDL + notify_*_change()- functie + AFTER INSERT/UPDATE-trigger op gedeeld channel

Server (Scrum4Me)

  • actions/questions.ts: Server Action met getSession + Zod + demo-blok + productAccessFilter + atomic updateMany
  • app/api/realtime/notifications/route.ts: user-scoped SSE met initial-state-event ná LISTEN actief (race-fix conform M10 ST-1004)
  • app/api/cron/expire-questions/route.ts: Bearer-auth via CRON_SECRET + updateMany WHERE expires_at<now

Client (Scrum4Me)

  • stores/notifications-store.ts: Zustand store met init/upsert/remove + selectors voor count en for-you-count
  • lib/realtime/use-notifications-realtime.ts: EventSource hook met state/message-handlers + reconnect-backoff + Page Visibility pause
  • components/notifications/notifications-bell.tsx + notifications-sheet.tsx
    • answer-modal.tsx: Bell met badge, slide-over met item-list, Dialog met free-text/options-radio

MCP-tools (mcp)

  • src/tools/ask-user-question.ts: write-tool met optionele wait_seconds- polling (intern setInterval tot status verandert of timeout)
  • src/tools/get-question-answer.ts: read-tool voor latere session-pickup
  • src/tools/list-open-questions.ts: read-tool voor session-start-check
  • src/tools/cancel-question.ts: write-tool, asker-only via atomic updateMany WHERE asked_by + status='open'

Wanneer dit patroon NIET gebruiken

  • Wanneer beide kanten al synchroon kunnen werken — dan is een gewone fetch/Server-Action eenvoudiger
  • Wanneer realtime niet kritiek is — een korte poll-loop is simpeler dan een SSE-stream
  • Wanneer er één centrale beslisser is — gebruik dan een gewone form-flow; het patroon hier is voor situaties waar de agent niet hoeft te wachten op één specifieke gebruiker

Referenties

  • Volledige flow + threat-model: docs/architecture.md § Vraag- antwoord-kanaal Claude ↔ user
  • Endpoint-contract: docs/api/rest-contract.md § Notifications + Cron
  • LISTEN/NOTIFY-pattern: app/api/realtime/solo/route.ts (M8 ST-802) — zelfde ReadableStream + heartbeat + hard-close + abort-cleanup
  • M10 vs M11 keuze tussen eigen/gedeeld kanaal: zie threat-model-tabel