Durable audit / scan jobs
Date : 2026-09-10. Pilier : ai-platform. Item : 0592. Prérequis UX : 0591 (preliminary __progress sur les tools chat).
Date : 2026-09-10. Pilier : ai-platform. Item : 0592.
Prérequis UX : 0591 (preliminary __progress sur les tools chat).
1. Problème
Deux chemins longs existent aujourd'hui et partagent le même plafond :
| Surface | Budget | Ce qui tourne |
|---|---|---|
POST /api/chat | maxDuration = 300 | runFullStoreAudit / runTrackingScan inline dans le tour modèle |
POST /api/tracking/scan | ~90–130 s côté tool | orchestrateur 41 checks + Browserbase éventuel |
Conséquences à l'échelle (centaines d'audits / min perçus comme vivants) :
- Coupe nette : la fonction meurt à 300 s même si le progress UI (0591) était honnête jusqu'ici.
- Pas de reprise : un refresh / network blip tue le travail ; 0566 (resume silencieux) n'a rien de durable à reprendre hors stream Upstash du tour chat.
- Pas d'idempotence produit : deux clics = deux scans qui se marchent dessus ; le ledger agent voit deux événements.
- OTP / cold path : lancer un audit pendant l'auth sans job = scan
orphelin. Politique actuelle : stash prompt +
?initialPrompt=(0590), jamais QStash pendant OTP.
maxDuration = 300 reste le bon plafond pour un tour de chat. Ce n'est
pas le plan d'échelle pour le travail d'audit.
2. Ce qui existe déjà (réutiliser, ne pas reinventer)
Le socle jobs est en production :
enqueue(type, payload, { deduplicationId, … })—src/services/jobs/client.ts- Registry handlers —
src/services/jobs/handlers/index.ts - Signature + worker
/api/jobs/[type] HEAVY_JOB_TYPES(aujourd'huiintelligence-scan-store) : refuse le fallback inline sur Vercel quand QStash manque- Quota / readiness :
heavyJobQueueReadiness(), latch 429
Le Spy deep-scan (intelligence-scan-store) est le modèle le plus proche :
minutes de travail, déduplication, pas d'inline en prod. Chat audit ≠ Spy
pipeline, mais le transport est le même.
0591 a fixé le contrat UI :
- preliminary yields
__progress(kind: audit | scan,phase,%,steps) - une seule carte de run,
AuditRunCard(barreui/progressponderee, etapes traduites, temps ecoule), puis le rapport enAuditCardView(ai-platform/3060) - les deux ouvrent sur le MEME en-tete (
cards/audit-report-header.tsx: favicon + hote, titre, anneau) ; l'anneau trace la progression pendant le run puis balaie jusqu'au score quand le rapport le remplace. Le rapport : jauge + bande, piliers en barres (pire en tete, non mesure hachure), plan d'action numerote par horizon avec « Lancer » par etape et « Lancer le plan avec @Atlas », constats par pilier en accordeon. Toutes les regles d'ordre vivent danscards/audit-card-model.ts, teste (ai-platform/3066) - Thinking caché dès qu'un tool tourne
Un job durable doit émettre le même shape (ou un miroir KV que la carte lit), pas inventer un second protocole UI.
3. Cible
Operator → @Atlas tool call
→ enqueue("store-audit" | "tracking-scan-durable", …)
→ yield __progress { phase: "queued", … }
→ QStash → /api/jobs/<type>
→ handler runFullStoreAudit / runTrackingScan
→ write progress to KV (jobId)
→ final report → Conversation / agent ledger / blob
→ chat resume (0566) OR tool poll / SSE replay
→ last yield = final report (même contrat 0591)
3.1 Job types (proposition)
JobType | Payload minimal | deduplicationId |
|---|---|---|
store-audit | { orgId, userId, storeId?, url, conversationId, toolCallId } | store-audit:${orgId}:${sha256(url)}:${day} ou …:${toolCallId} si force |
tracking-scan-durable | { orgId, userId, storeId, url, conversationId, toolCallId } | tracking-scan:${storeId}:${sha256(url)}:${hour} |
Les deux entrent dans HEAVY_JOB_TYPES.
Idempotence : une clé par (org/store, cible, fenêtre). Un second
enqueue dans la fenêtre renvoie le messageId / jobId existant ; le
tool chat se rattache au progress existant au lieu de relancer.
3.2 Progress ledger (Upstash KV)
Clé : audit:progress:${jobId} (TTL 24 h).
Valeur : le même objet que AuditScanProgress (0591), plus
{ status: "queued" | "running" | "succeeded" | "failed", updatedAt }.
Le tool execute async generator :
enqueue→ yield queued- poll KV (ou SSE interne) → yield progress
- terminal → yield rapport final (ou erreur typée)
Pas de second composant UI : message-tool-part + AuditRunCard
(features/ai/chat/runtime/audit-run-part.tsx) restent la surface.
3.3 Concurrence
- Cap par org (ex. 2 audits lourds simultanés) avant enqueue → 429 métier / message tool clair.
- Cap global aligné sur le quota QStash (lire
heavyJobQueueReadinessavant fan-out). - Browserbase / tracking : réutiliser les timeouts existants ; le job
QStash timeout doit être ≥ le budget scan (130 s tool aujourd'hui),
pas le défaut 3 retries trop courts sans
retries/timeoutexplicites surenqueue.
3.4 Crédits et auth
- Estimation / hard cap crédits : avant enqueue (même garde que le chat), pas après coup sur le worker.
- Cookie de session : ne pas rejouer le cookie dans QStash. Le
worker authentifie par signature QStash + payload signé
(
orgId/userId/storeIddéjà validés au moment du tool call). Le bridge Shopify du worker se reconstruit depuis le storeId (même pattern que les autres jobs store-scoped).
3.5 Ce qu'on ne fait pas
- Audit / scan pendant OTP ou avant
isTenantReady(0590). - Nouvelle famille de credentials (ADR 0007).
- Remplacer le Spy
intelligence-scan-storepar ce job (pipelines distincts ; chat audit ≠ corpus Spy). - Gonfler
maxDurationde/api/chatau-delà de 300 pour "tenir" les audits : ça déplace le problème.
4. Découpage d'implémentation (PRs suivantes)
| PR | Contenu | Pilier |
|---|---|---|
| A | JobType + handler no-op / log + tests registry | ai-platform (+ platform-ops si vercel.json / worker) |
| B | Progress KV + tool generators branchés enqueue | ai-platform |
| C | Caps org + readiness messaging | ai-platform |
| D | Resume chat (0566) sur jobId | ai-platform |
Chaque PR = un item backlog. Celle-ci (0592) ne contient que cette
doc.
5. Critères "le monde en parle" à l'échelle
- 50 audits démarrés en 1 minute sur des orgs distinctes : tous
queued→runningsans 5xx chat. - Double-clic même URL : un seul job, deux tool cards accrochées au même progress.
- Kill du tab navigateur à 50 % : retour conversation → reprise ou état terminal visible (pas de boîte noire).
- QStash quota exhausted : un message opérateur actionnable (pas N
enqueue_failedsilencieux), chat reste utilisable.
6. Références code
- Chat budget :
src/app/api/chat/route.ts(maxDuration = 300) - Progress UI :
src/features/ai/store-audit/progress.ts,src/components/patterns/ai-elements/chat/audit-run-card.tsx(AuditRunCard) - Tools :
src/features/ai/tools/store-audit-tool.ts,src/features/ai/tools/tracking-scan.ts - Jobs :
src/services/jobs/client.ts,types.ts,handlers/index.ts - Heavy precedent :
intelligence-scan-store+HEAVY_JOB_TYPES