Skip to content

feat(jobs,extraction): process requests in the worker with retries and visible errors - #34

Merged
Fluory merged 68 commits into
mainfrom
claude/feat-jobs-worker-7
Sep 23, 2026
Merged

Fluory merged 68 commits into
mainfrom
claude/feat-jobs-worker-7

Conversation

@Fluory

@Fluory Fluory commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Warum

Fixes #7 · Teil von Epic #2 · gestapelt auf #33 (Basis claude/feat-ai-service-6).

Arbeitsstand

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)

  • Kein Auslöser – keine Modulgrenze, öffentliche API, Migration, Auth/Rechte, kein Zahlungs-/Daten-/Infrapfad, höchstens zwei Module, keine Architekturvarianten, umkehrbar
  • Auslöser zutreffend – Impact Manifest ausgefüllt (Plan vor Code)

Impact Manifest

  • Betroffene Module: jobs (Handler, drain(), Dead-Letter, Reprocess, Zeitbudget, Worker), extraction (KI-Client, Merge, Repository), requests (Statusmaschine, Sperre, Übergänge, Fehlerzustand), observability (logEvent nur mit IDs), db (Migrationen 0006–0008, pg-boss-Client-Optionen und Grants), config (AI_SERVICE_*), compose.yaml (Profil ai).
  • Schnittstellen / Datenänderungen: Aufruf POST /v1/extract des KI-Dienstes (Vertrag contracts/ai-service.openapi.yaml, Bearer, X-Request-Id); Tabellen app.extraction_runs (unique job_id), app.extraction_segments, app.extracted_fields (Check: found nur mit Zitat; Beleg muss auf ein gespeichertes Segment zeigen) – alle FORCE RLS + Composite-FKs; app.requests + error_stage, error_message, attempts, next_retry_at.
  • Akzeptanzkriterien: alle aus feat(jobs,extraction): process requests in the worker with retries and visible errors #7 (Nachweis unten).
  • Testplan: Unit test-first: Statusmaschine, Feld-Merge, KI-Client gegen HTTP-Stub (Retry-/Permanent-Klassen, Timeout, Vertragsprüfung), Zeitbudget; Integration gegen echtes Postgres + SeaweedFS + pg-boss mit KI-Stub an der HTTP-Grenze und eigenen Test-Queues (schnelle Retries, kurzer Ablauf).
  • Verifizierte Fakten: pg-boss 12.33.6 – Wartung explizit über boss.supervise(name?) (ADR-Offenpunkt D4): gedrosselt über monitorIntervalSeconds/maintenanceIntervalSeconds; als app_rw braucht sie UPDATE auf pgboss.version; persistQueueStats muss aus sein (legt täglich Partitionen per DDL an). Ein abgelaufener, nicht abgeschlossener Job geht über supervise zurück in retry (Integrationstest). Job-Ablauf 1 h, beim Worker-Start gegen AI_SERVICE_TIMEOUT_MS × UPLOAD_MAX_FILES geprüft.
  • Offene Annahmen: Retry-Parameter (5 Versuche, 30 s … 30 min) – mit dem Kunden abzustimmen.
  • Nicht-Ziele: Vercel-drain()-Trigger, Export, UI.
  • Risiken und Rollback: Dokumentinhalt geht an den KI-Dienst (nur EU-Endpunkt, nur synthetisch lokal); Logs nur mit IDs (Test); Migrationen additiv; Rollback per Revert.

Akzeptanzkriterien → Nachweis

Kriterium Nachweis
pg-boss 12, transaktionales Enqueue (fromDrizzle), singletonKey = requestId, Retry mit Backoff, Dead-Letter Queue-Definition jobs/queues.ts; Enqueue aus #5 (sendInTransaction); processing.test.ts („retries … then dead-letters"), Reprocess-Test prüft den Job mit singletonKey
Handler idempotent: doppelte Zustellung → genau ein Lauf processing.test.ts: nacheinander und gleichzeitig (Promise.all) → ein Lauf, einmal REVIEW; Wiederanlauf nach Worker-Absturz (Ablauf → supervise → Retry) → ein Lauf; Job mit fremder companyId wird übersprungen
KI-Timeout/5xx → Retry; nach Limit → ERROR mit Ursache und Versuchen; Reprocess stellt neu ein processing.test.ts (503 → Retry → Dead-Letter → ERROR „Der KI-Dienst ist nicht erreichbar.", attempts 2; Timeout retrybar; 401 sofort ERROR; Reprocess ERROR → NEW + Job + Audit; fremde Firma abgelehnt). Anzeige in der Liste: #26
drain({ maxMs }) bis zum Budget; Worker-Schleife nutzt es jobs/drain.ts, src/worker.ts; alle Integrationstests laufen über drain
Logs mit requestId, jobId, companyId, ohne Dokumentinhalt processing.test.ts („logs IDs only"): Log-Zeilen enthalten die requestId, aber keinen Firmennamen, keine Namen, keine E-Mail-Adressen, kein Datum

Geä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, Migrationen 0006_processing.sql, 0007_processing_force_rls.sql, 0008_processing_evidence_fks.sql; src/db/job-queue-client.ts (persistQueueStats: false, UPDATE auf version, monitorIntervalSeconds für Tests).
  • src/config/env.ts (+Test), .env.example, compose.yaml (Worker-Env, Profil ai), .gitignore (.secrets/).
  • Tests: tests/integration/processing.test.ts.
  • Doku: data-model.md, Architekturkarte, operations.md (Worker, KI-Profil, Wiederanlauf, bekannte Lücke), CHANGELOG.

Nachweis (SYSTEM.md §11)

  • verify:changed: grün
  • verify: 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 betroffen
  • Manueller Prüfnachweis: pnpm worker startet, verarbeitet, stoppt sauber auf SIGTERM; ohne AI_SERVICE_TOKEN bricht der Start mit Variablennamen ab. Echter KI-Dienst mit Vertex: nicht geprüft (keine Credentials).
  • Frischer Review (P1 vor Ready-for-review; Architektur/API/DB immer): feat(jobs,extraction): process requests in the worker with retries and visible errors #34 (comment) – alle Befunde behoben

Doku-Entscheidung (genau eine)

  • Keine langlebige Doku betroffen – Begründung: –
  • Doku betroffen und im selben PR aktualisiert:
    • Produktdoku (P0: README): –
    • Technische Doku: docs/technical/data-model.md, docs/technical/operations.md
    • Architekturkarte: docs/technical/architecture.md
    • ADR: –
    • CHANGELOG [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):

  • keine
  • bewusst belassen – Begründung: –
  • im selben PR nach fachlicher Verantwortung geteilt
  • Folge-Issue –

Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen

Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:

Subagent-Einsätze

  • Frischer Review: unabhängiger Subagent (read-only) – 5 Important, 3 Notes, alle bearbeitet.

Risiken / offene Punkte


🤖 Generated with Claude Code

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
…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
…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
…ests, docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
@Fluory Fluory mentioned this pull request Sep 23, 2026
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
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
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
- 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
…t concurrency test; ops notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1

Fluory commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

Frischer Review (unabhängiger Subagent) – Befunde und Auflösung

Review nach review-pr + Migrations-/Test-/AI-/Security-Regeln, read-only; pg-boss-12.33.6-Quellen gegengelesen (Dead-Letter behält data; fail/getJobById/Zustandsnamen korrekt; Wartung als app_rw baut keine Indizes neu). Auflösung in den Commits nach 346ff1c.

# Befund Auflösung
Important Job-Ablauf (10 min) kürzer als der mögliche Verarbeitungsweg (10 Dokumente × 120 s) → Neuzustellung während der Arbeit behoben – Ablauf 1 h; assertProcessingBudget prüft beim Worker-Start AI_SERVICE_TIMEOUT_MS × UPLOAD_MAX_FILES + Reserve < Ablauf (fail-closed, Unit-Test)
Important recordProcessingFailure schrieb ohne Statusprüfung (Fehlertext auf REVIEW-Anfrage möglich) behoben – nur bei PROCESSING
Important Antwort mit found ohne Beleg, unbekanntem Segment, doppelten Segment-IDs oder fremder documentId lief in 5 teure Retries behoben – Vertragsinvarianten im Client → contract_violation (permanent, 4 neue Tests); Constraint-Verletzungen beim Speichern → permanent; DB erzwingt jetzt: Beleg zeigt auf gespeichertes Segment (Composite-FK, Migration 0008)
Important Wiederanlauf nach Worker-Absturz (DR4) unbewiesen behoben – Integrationstest: Job geholt, Anfrage geclaimt, „Absturz", Ablauf, supervise() als app_rw → retry → drain → REVIEW, attempts 2, genau ein Lauf
Important Dead-Letter nur bei leerer Queue; Fehler beim Markieren → Anfrage bleibt still in PROCESSING behoben – jede Runde ein Dead-Letter-Job; die Dead-Letter-Queue wiederholt ihren eigenen Handler (10×, Backoff). Verbleibende Lücke (Job ganz verschwunden) dokumentiert in operations.md, Sweep folgt mit #26/#28
Note 400/404/405 als Dokumentfehler eingestuft behoben – Dienst-Fehler (falsche URL/Client-Bug), Tests
Note Nebenläufigkeitstest behauptete mehr als er prüft; Reprocess-Test ließ Job in der geteilten Queue behoben – Test umbenannt und prüft KI-Aufrufe (≤ 2) = attempts; Aufräumen der geteilten Queue; neuer Test: Job mit fremder companyId wird übersprungen
Note data-model-Tabelle zerbrochen; extracted_fields.request_id ohne FK behoben – Tabelle repariert; Composite-FK (request_id, company_id) (Migration 0008, auf frischer DB getestet)

Verdict des Reviewers: nach Behebung mergebar, menschliche Freigabe nötig (Migrationen 0006–0008, pg-boss-Grants).


Generated by Claude Code

@Fluory
Fluory marked this pull request as ready for review September 23, 2026 06:50
@Fluory
Fluory changed the base branch from claude/feat-ai-service-6 to main September 23, 2026 09:59
@Fluory
Fluory merged commit 3885891 into main Sep 23, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(jobs,extraction): process requests in the worker with retries and visible errors

2 participants