Repository navigation
feat(jobs,extraction): process requests in the worker with retries and visible errors - #34
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ion access control Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…LS with integration proofs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…for #4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…boss queues Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…nd download routes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…-only audit, composite FK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
- sign-up requires the invitation id (link) plus the invited e-mail – no takeover by address alone - one company per user (unique index), deterministic membership lookup, actor from membership - organization plugin accepts only admin/clerk roles - configurable client-IP source for the auth rate limit; local secret refused in production - invite page: zod input, 404 for clerks, shows the invitation link; signup needs the link - tests: wrong/missing invitation id, foreign set-active/list-members, last admin, roles - docs: operations (rate limit/proxy, recovery), data model, exceptions register (admin plugin) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…e rejection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
… docs for #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…secret The image sets NODE_ENV=production, so the previous check blocked the local compose stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…re script Python 3.13 package requestflow_ai (src layout), docling/fastapi/google-genai pinned, CPU-only torch via the PyTorch CPU index, dev tools ruff/pyright/pytest. The synthetic PDF fixture is generated by scripts/make_fixtures.py (reportlab). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Test-first per ADR-0001 D8: quote normalisation (whitespace, case, NFKC, hyphenation), German number and date formats, and the verifier rules that turn unsupported model claims into unverified. Also adds the segment and model-output types the tests build on; the verifier does not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
normalize_text folds NFKC, soft hyphens, line-break hyphenation, typographic dashes/quotes, whitespace and case. values parses German/ISO numbers and DD.MM.YYYY, D.M.YY and ISO dates. verify_field only keeps or downgrades the model's status: a quote not in the cited segment, an unknown segment, a value inconsistent with the quote, found without evidence or value, and missing with a value all become unverified with a reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ction (red) EML: body lines with 1-based line locators, From/Subject header segments, RFC 2047 and quoted-printable decoding, HTML-only bodies, header newline collapse, no attachment payloads. PDF: textline segments with page + top-left bbox via docling-parse (model-free); the layout-pipeline test only runs with AI_TEST_DOCLING_MODELS=1. Detection by magic bytes; .msg rejected for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
EML via the standard library email package (policy=default): From/Subject header segments and one segment per non-empty body line, HTML-only bodies reduced to text lines. docling's EMAIL backend was checked but emits paragraphs without provenance, so it cannot give line locators. PDF via docling: the default textlines pipeline reads docling-parse text lines (page + top-left bbox, no ML models, no network); the opt-in layout pipeline uses DocumentConverter (OCR and tables off) and the heron layout model. docling is imported lazily. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ex model client (red) The model client is exercised through the real google-genai SDK with an httpx MockTransport replaying recorded generateContent bodies: eu multi-region URL, bearer auth, structured-output config, token usage, fail-closed init (no project, no credentials, dev flag without key), no silent switch to API key mode, and schema-invalid output. Adds the env-based Settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…lient Prompt extract_header_v1.md (system instruction) plus render_document, which puts segment-id-prefixed lines between <document> delimiters and neutralises delimiter-like tags inside the document. GeminiModelClient calls Vertex via google-genai with response_schema = ModelExtraction, JSON mime type, temperature 0 and no tools; schema-invalid output raises ModelOutputError. build_model_client needs VERTEX_PROJECT and credentials, loads ADC eagerly, refuses API-key mode for Vertex, and allows the Gemini API only with AI_ALLOW_GEMINI_API_DEV=true plus GEMINI_API_KEY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Pipeline: PDF and EML end to end with recorded model responses, a model date contradicting its own quote, the prompt-injection mail (the recorded model obeys and invents a quote -> unverified), and the no-text PDF that skips the model. API: bearer auth (401 + WWW-Authenticate), response shape, request id handling, 400/413/415/422/429/502 mapping without echoing input, JSON logs with IDs only. Contract: the committed OpenAPI file equals the app's schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ests, docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
7 tasks done
…tes (red) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ries A verified value is returned normalised (dates as YYYY-MM-DD, text trimmed). Text values must match the quote on word boundaries, so partial tokens are unverified. A date quote holding several distinct dates caps the field at uncertain (reason ambiguous_quote). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
The two existing dev-flag tests now set vertex_project=None in their setup (make_settings defaults it); their assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…are both set Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…quest id (red) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…inside request context ExtractGuard (pure ASGI) rejects /v1/extract without a valid bearer token (401), with a missing or non-numeric Content-Length (411, new error code length_required) or with a declared length above the limit plus a 16 KiB multipart allowance (413) before any body byte is read. Unexpected errors are caught inside the request context: logged with requestId/documentId and the exception type only, answered with requestId and X-Request-Id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
AI_MAX_PDF_PAGES (default 50) rejects longer PDFs with 422 document_too_long before any page is parsed (model-free page count, also before the layout model runs). With AI_PDF_PIPELINE=layout the converter and its layout model are built in create_app, so a missing model stops the service start (PdfPipelineInitError) instead of failing every request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…p and native stderr Fixtures are described as hand-written in the Vertex REST format (not recorded). New env var AI_MAX_PDF_PAGES, 411/422 codes, dev-flag ambiguity, layout startup check. docling-parse/qpdf stderr bypasses JSON logging; no output observed, whether it can carry document text is unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
New error codes length_required and document_too_long, 411 response, reason ambiguous_quote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Behind uvicorn --root-path the raw scope path carries the prefix, and the guard would silently skip /v1/extract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
… root path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
7 of 15 tasks
- AI response invariants (found ⇒ evidence, cited segment exists, unique ids, same document) → permanent - 400/404/405 are service errors, not document errors - processing time budget checked against the job expiry (1 h) at worker start - failure state recorded only while PROCESSING; constraint violations on persist are permanent - dead-letter jobs handled every round, dead-letter queue retries its own handler - schema: fields pinned to request (same company) and to their evidence segment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…t concurrency test; ops notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Owner
Author
Frischer Review (unabhängiger Subagent) – Befunde und AuflösungReview nach
Verdict des Reviewers: nach Behebung mergebar, menschliche Freigabe nötig (Migrationen 0006–0008, pg-boss-Grants). Generated by Claude Code |
This was referenced Sep 23, 2026
Fluory
marked this pull request as ready for review
September 23, 2026 06:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warum
Fixes #7 · Teil von Epic #2 · gestapelt auf #33 (Basis
claude/feat-ai-service-6).Arbeitsstand
drain()(Showcase-Epic), Export (feat(export): export approved requests exactly once to the ERP mock #9), Review-UI (feat(review): review fields beside their source, correct, approve or reject #8), Liste mit Fehlern (feat(requests): request list with errors, retries and reprocessing #26).drain(), Dead-Letter → ERROR, Reprocess, Worker-Schleife mit Zeitbudget-Prüfung, Compose-Profilai; frischer Review eingearbeitet (Kommentar);pnpm verifylokal grün; Migrationen auf frischer DB getestet.Was ist passiert (Klartext)
Hochgeladene Anfragen werden jetzt im Hintergrund verarbeitet. Der Worker holt sich den Auftrag, schickt jedes Dokument an den KI-Dienst und speichert die gefundenen Angaben mit ihrer Fundstelle. Erst wenn alles gespeichert ist, springt die Anfrage in einem Schritt auf „zur Prüfung". Kommt derselbe Auftrag doppelt an (das kann bei Warteschlangen passieren), entsteht trotzdem nur ein Ergebnis.
Ist der KI-Dienst kurz nicht erreichbar, versucht das System es später erneut, mit wachsendem Abstand. Klappt es nach mehreren Versuchen nicht, steht die Anfrage auf „Fehler" – mit einer verständlichen Ursache, der Zahl der Versuche und ohne technische Details. Mitarbeitende können sie dann neu anstoßen. Fehler, die ein Wiederholen nicht behebt (z. B. ein falsch eingerichteter KI-Dienst), gehen sofort auf „Fehler". Ein einzelnes unlesbares Dokument bremst die anderen Dokumente derselben Anfrage nicht aus. Stürzt der Worker mitten in der Arbeit ab, wird der Auftrag nach Ablauf automatisch erneut ausgeführt.
Plan-Pflicht (SYSTEM.md §4)
Impact Manifest
jobs(Handler,drain(), Dead-Letter, Reprocess, Zeitbudget, Worker),extraction(KI-Client, Merge, Repository),requests(Statusmaschine, Sperre, Übergänge, Fehlerzustand),observability(logEventnur mit IDs),db(Migrationen 0006–0008, pg-boss-Client-Optionen und Grants),config(AI_SERVICE_*),compose.yaml(Profilai).POST /v1/extractdes KI-Dienstes (Vertragcontracts/ai-service.openapi.yaml, Bearer,X-Request-Id); Tabellenapp.extraction_runs(uniquejob_id),app.extraction_segments,app.extracted_fields(Check:foundnur mit Zitat; Beleg muss auf ein gespeichertes Segment zeigen) – alle FORCE RLS + Composite-FKs;app.requests+error_stage,error_message,attempts,next_retry_at.boss.supervise(name?)(ADR-Offenpunkt D4): gedrosselt übermonitorIntervalSeconds/maintenanceIntervalSeconds; alsapp_rwbraucht sie UPDATE aufpgboss.version;persistQueueStatsmuss aus sein (legt täglich Partitionen per DDL an). Ein abgelaufener, nicht abgeschlossener Job geht übersupervisezurück inretry(Integrationstest). Job-Ablauf 1 h, beim Worker-Start gegenAI_SERVICE_TIMEOUT_MS × UPLOAD_MAX_FILESgeprüft.drain()-Trigger, Export, UI.Akzeptanzkriterien → Nachweis
fromDrizzle),singletonKey = requestId, Retry mit Backoff, Dead-Letterjobs/queues.ts; Enqueue aus #5 (sendInTransaction);processing.test.ts(„retries … then dead-letters"), Reprocess-Test prüft den Job mitsingletonKeyprocessing.test.ts: nacheinander und gleichzeitig (Promise.all) → ein Lauf, einmal REVIEW; Wiederanlauf nach Worker-Absturz (Ablauf →supervise→ Retry) → ein Lauf; Job mit fremdercompanyIdwird übersprungenprocessing.test.ts(503 → Retry → Dead-Letter → ERROR „Der KI-Dienst ist nicht erreichbar.",attempts2; Timeout retrybar; 401 sofort ERROR; Reprocess ERROR → NEW + Job + Audit; fremde Firma abgelehnt). Anzeige in der Liste: #26drain({ maxMs })bis zum Budget; Worker-Schleife nutzt esjobs/drain.ts,src/worker.ts; alle Integrationstests laufen überdrainprocessing.test.ts(„logs IDs only"): Log-Zeilen enthalten die requestId, aber keinen Firmennamen, keine Namen, keine E-Mail-Adressen, kein DatumGeändert
src/features/requests/status.ts(+Test),repository.ts(Sperre, Übergänge, Fehlerzustand).src/features/extraction/ai-client.ts(+Test),merge.ts(+Test),repository.ts,types.ts,fixtures.ts.src/features/jobs/process-request.ts,drain.ts,reprocess.ts,budget.ts(+Test);src/worker.ts.src/features/observability/log.ts.src/db/schema/app.ts, Migrationen0006_processing.sql,0007_processing_force_rls.sql,0008_processing_evidence_fks.sql;src/db/job-queue-client.ts(persistQueueStats: false, UPDATE aufversion,monitorIntervalSecondsfür Tests).src/config/env.ts(+Test),.env.example,compose.yaml(Worker-Env, Profilai),.gitignore(.secrets/).tests/integration/processing.test.ts.data-model.md, Architekturkarte,operations.md(Worker, KI-Profil, Wiederanlauf, bekannte Lücke), CHANGELOG.Nachweis (SYSTEM.md §11)
verify:changed: grünverify: grün lokal – lint, typecheck, 100 Unit + 56 Integrationstests, depcruise 0 Verstöße, build, audit 0 high; Migrationen 0000–0008 auf frischer DB, 56/56 grün. CI: siehe Checks dieses PRs.verify:full/ E2E-Spec: nicht betroffenpnpm workerstartet, verarbeitet, stoppt sauber auf SIGTERM; ohneAI_SERVICE_TOKENbricht der Start mit Variablennamen ab. Echter KI-Dienst mit Vertex: nicht geprüft (keine Credentials).Doku-Entscheidung (genau eine)
docs/technical/data-model.md,docs/technical/operations.mddocs/technical/architecture.md[Unreleased](sichtbares Feature oder Verhalten – im selben PR, nie „später")Entferntes oder Umbenanntes: nichts entfernt
Dateigrößen und neue Bausteine (SYSTEM.md §7)
Dateien über 500 Zeilen im Diff (Ausnahmen: generierter Code, Lockfiles, Fixtures, Migrationen, Schemas, Ressourcen, Doku, Konfiguration):
Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen
Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:
src/; gefunden: nurobservability/health– KI-Client (extraction) undlogEvent(observability) sind in der Architekturkarte vorgesehen; pino folgt mit feat(observability): correlated structured logs and full health #28Subagent-Einsätze
Risiken / offene Punkte
.xlsx/.docx/.msgwerden bis feat(ai-service): XLSX, DOCX, MSG and scanned PDFs #23 übersprungen (Original bleibt, Hinweis im Lauf).🤖 Generated with Claude Code
https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1