# AA-047 – Maintenance-Sentinel und Provider-Rate-Limit **Empfänger:** Hermes Carlo **Prüfzeitraum:** 28.08.2026, UTC; Endaudit 21:54:15 UTC. **Scope:** maximal erforderliche Einzelpiloten; keine Massenabarbeitung; kein Queue-Timer aktiviert. ## 1. Ergebnis Der Maintenance-Codepfad ist vollständig lokalisiert. `/maintenance/status` und der `/episodes`-Write-Gate lesen denselben Sentinel `/run/graphiti-control/maintenance.lock`. Der frühere Widerspruch war ein zeitlicher Wechsel des Sentinel-Zustands. Die konkrete externe Erzeuger-/Löscher-Identität konnte trotz statischer Suche und 240-sekündigem Laufzeit-Watch nicht nachgewiesen werden: **Ursache noch nicht nachgewiesen**. Der Providerfehler von Queue-ID 876 ist eindeutig als **RATE_LIMIT** klassifiziert: Graphiti/Provider meldete intern HTTP 429, temporäre Upstream-Drosselung des gemeinsam genutzten Pools. Das ist kein nachgewiesenes lokales Tages-/Monatskontingent. Der Graphiti-Service gibt diesen Fehler jetzt als HTTP 429 statt pauschal HTTP 500 aus; der Single Writer setzt ihn kontrolliert auf `retry_wait` mit zukünftigem `next_attempt_at`. Queue-ID 4 bleibt erfolgreich. Queue-ID 876 konnte trotz Provider-Verfügbarkeitsprobe nicht erfolgreich verarbeitet werden und erhielt erneut HTTP 429. Gemäß Fail-Stop wurden Pilot 3 und weitere Retries nicht gestartet. Der 3er-Pilot ist daher nicht erreicht. ## 2. Baseline und Backups Baseline: 28.08.2026 21:41:34 UTC. Queue-ID 876 war `queued`, Versuchszähler 0, keine Episode; Queue→Neo4j-Audit: 630 eindeutige Queue-`done`-IDs, 630 nachgewiesen, 0 fehlend. AA-047-Backup vor Änderungen: `/opt/struktur/reports/aa047-backup/20260828T214134Z/`. Manifest mit Originalpfad, Backuppfad, Größe, Mtime und SHA-256: `/opt/struktur/reports/AA-047-baseline.json`. Wichtige Backups: - `/opt/struktur/graphiti/service/main.py` → `.../graphiti/main.py`, 54.842 Bytes, SHA-256 `b4bcf4789f56b03d2e4517c0fbdc589c587afebfef3a44a4c677898e8f46e206` - `aa043_single_writer.py` → `.../workers/aa043_single_writer.py`, 5.888 Bytes, SHA-256 `fcf480fda4f02c7544701de943c599a892ab4b00450315ef59592fae063c4b61` - `e2e_worker.py` → `.../workers/e2e_worker.py`, 17.427 Bytes, SHA-256 `9427e881e6a0d58b092e3561b74eef8c8133dc5b34507e49929d4885933bc25f` - `knowledge.db` → `.../data/knowledge.db`, 153.182.208 Bytes, SHA-256 `66003b4f7263dbd49d3cae2f2fac21b2ddfb49e0eadd16d93a40ee76ff53314a` - Relevante Systemd-/Compose-Dateien sind ebenfalls im Manifest enthalten. ## 3. Maintenance-Sentinel: Codepfad Produktive Source: `/opt/struktur/graphiti/service/main.py`. - Zeile 507: `GRAPHITI_MAINTENANCE_LOCK`, Default `/run/graphiti-control/maintenance.lock`. - Zeilen 662–664: `maintenance_status()` ruft `Path(MAINTENANCE_LOCK).exists()` auf und leitet daraus `maintenance_mode`, `write_requests_allowed` und `lock_exists` ab. - Zeilen 669–671: `/episodes` ruft dieselbe `Path.exists()`-Prüfung auf. Bei vorhandenem Sentinel: HTTP 503, `error_type=maintenance_mode`, `retryable=true`. - Container-Mount: `/run/graphiti-control` wird in `graphiti-service` read-only eingebunden. - Aktiver Container: ein Uvicorn-Prozess, kein nachgewiesener Mehrprozess-Zustand. Es wurden kein zweiter Maintenance-Wert, kein Request-Cache und kein separater Worker-Zustand gefunden. `/health` verwendet ebenfalls denselben Sentinelpfad, ergänzt um Readiness-/Providerfelder. ## 4. Erzeuger-Inventur und Laufzeitnachweis Statische Suche wurde in `/opt/struktur`, Graphiti-/Importer-Code, relevanten Systemd-Units/Timern, Cron-Konfigurationen, Deployment-/Backup-Pfaden und Container-Mounts durchgeführt. Treffer waren Leser, Compose-Mounts, Backups oder Staging-Run-Block-Logik; kein produktiver Schreibpfad für genau `/run/graphiti-control/maintenance.lock` wurde nachgewiesen. Temporärer Watch: Linux-inotify auf dem Verzeichnis `/run/graphiti-control`, Filter `maintenance.lock`, Ereignisse CREATE/WRITE/DELETE/MOVE, Laufzeit 240,2 Sekunden. Ergebnis: `events=[]`. Damit wurde in diesem Beobachtungsfenster kein Erzeuger/Löscher beobachtet. Der Watch-Prozess und die temporäre Scriptdatei wurden nach Ablauf beendet; keine dauerhafte Audit-Regel wurde gesetzt. Bewertung: Der Sentinel-Schutz ist technisch korrekt und darf nicht entfernt werden. Der konkrete historische Erzeuger aus AA-046 bleibt **nicht nachgewiesen**. Eine Race Condition ist als zeitliche Erklärung des AA-045-Widerspruchs nachgewiesen, nicht als Identität des Verursachers. ## 5. Providerfehler Queue-ID 876 Autoritative Request-Quelle: `/opt/struktur/graphiti/request-data/requests.db`, Tabelle `graphiti_requests`, Queue-ID-Verknüpfung über `queue_id`. Historischer Pilot-2-Request aus AA-046: - Request-ID `c607caa9-a6ee-49b9-a1f5-28773a7677de` - Queue-ID `876` - Start `2026-08-28T21:24:04.183591+00:00`, Ende `21:24:54.642865+00:00` - Graphiti HTTP `500`, Phase `graphiti_processing` - intern: `RateLimitError`, Provider-HTTP `429` - Modell `qwen/qwen3.7-flash` - Graphiti-Provider-Metadatum `Nvidia`; Upstream-Metadatum `Alibaba` - `provider_error_code=insufficient_quota` - `limit_source=upstream_provider_shared_pool` - Upstream-Text: Modell temporär rate-limited, retry shortly; kein belastbarer lokaler Account-/Billing-Nachweis - `llm_request_count=10` Die gespeicherte Providerantwort enthielt keine zuverlässig persistierten `Retry-After`-/Rate-Limit-Header; daher konnte daraus kein exakter Provider-Resetzeitpunkt abgeleitet werden. AA-047-Retry-Request: - Request-ID `87ac23f7-9e64-41ae-bf4f-763135eafe79` - Queue-ID `876` - Start `21:50:24.674298+00:00`, Ende `21:53:36.429614+00:00` - Graphiti HTTP `429`, Phase `provider_rate_limit` - Modell `qwen/qwen3.7-flash`, Provider-Metadatum `Nvidia` - `llm_request_count=13` - Responseklasse: `provider_rate_limit`, `retryable=true`, keine Episode Klassifizierung: **RATE_LIMIT**. Begründung: tatsächlicher HTTP-429, explizit temporäre Upstream-Drosselung und `upstream_provider_shared_pool`. `insufficient_quota` ist hier ein Upstream-Fehlercode, aber kein Nachweis einer lokalen Tages-/Monats-/Billing-Quota. `QUOTA_EXHAUSTED` ist nicht nachgewiesen; `LOCAL_CLASSIFICATION_ERROR` ist widerlegt. ## 6. Reparaturen Keine Provider- oder Modelländerung. Keine Gate-Deaktivierung. Geändert und verifiziert: 1. Graphiti-Service: gekapselte Provider-Rate-Limit-Fehler werden aus dem Worker-Fehlerpfad erkannt und als HTTP 429 mit `error=provider_rate_limit`, `retryable=true`, `episode_created=false` ausgegeben; HTTP 500 bleibt für andere unbekannte Graphiti-Fehler. 2. Single Writer: HTTP-429-/Rate-Limit-Antworten werden separat erkannt. Der Queue-Eintrag erhält `graphiti_status='retry_wait'`, `remote_outcome='PROVIDER_RATE_LIMIT'`, `last_http_status=429`, `last_error_class='provider_rate_limit'`, freigegebene Locks und ein `next_attempt_at` mindestens 900 Sekunden in der Zukunft. `attempt_count` wird nicht künstlich zurückgesetzt. 3. Keine sofortige Wiederholung, kein Endlos-Retry und kein permanent failure. Syntaxprüfung beider geänderter Python-Dateien: OK. Graphiti-Image neu gebaut und Container nur deshalb neu erstellt; `/health` danach HTTP 200. ## 7. Provider-Verfügbarkeit vor Retry Kleiner separater Provider-Check aus dem aktiven Graphiti-Container: `/models` HTTP 200; konfiguriertes Modell war in der Modellliste vorhanden. Dieser Check beweist Erreichbarkeit/Auth/Modellauflistung, aber keine garantierte Request-Kapazität. Der anschließende Einzelpilot zeigte, dass die eigentliche Generierung weiterhin vom Upstream-429 blockiert wird. ## 8. Queue-ID 876 und Pilotstatus Vor dem Retry: `queued`, keine Episode, Preflight weiterhin eindeutig; Maintenance `false`, Write erlaubt, Provider-Modellauflistung erreichbar. Writer: `/opt/struktur/obsidian-graphiti-import/aa043/aa043_single_writer.py --queue-id 876`. Ergebnis: Exit 1 wegen HTTP 429. Danach read-only verifiziert: - Queue `retry_wait` - `next_attempt_at=2026-08-28T22:08:36.432864+00:00` - `remote_outcome=PROVIDER_RATE_LIMIT` - `last_http_status=429` - `last_error_class=provider_rate_limit` - `lock_owner=NULL`, `unknown_since=NULL` - `graphiti_episode_id=NULL` Pilot 3 wurde nicht gestartet. Weitere Retries wurden nicht ausgeführt. ## 9. Pilot-Tabelle | Queue-ID | Ausgangsstatus | Video-ID | Preflight | HTTP | Ergebnis | Episode | |---:|---|---|---|---:|---|---| | 4 | retry_wait | `2gvFLFl4xw8` | B, eindeutig | 200 | `WRITE_CONFIRMED` | `8de29adb-1b8d-4654-aed9-3dd17ceee91d` | | 876 | queued | `PQBYZQqan2g` | B, eindeutig | 429 | `PROVIDER_RATE_LIMIT`, Fail-Stop | keine | | 3 | nicht ausgeführt | – | – | – | nicht gestartet | – | Für Queue-ID 4 wurde das UUID-Inventar unabhängig geprüft: genau eine Episode mit Queue-Metadatum `queue_id=4` und `source_identity=youtube:2gvFLFl4xw8`. Für Queue-ID 876 wurde keine Episode erzeugt. Doppelwrites: 0 nachgewiesen für den erfolgreichen Pilot 4; für 876 ebenfalls keine Episode. ## 10. Queue→Neo4j-Endaudit Auditwerkzeug: `/opt/struktur/graphiti/youtube_queue_neo4j_audit.py`; Version `queue-done-neo4j-complete-property-join-v2`; read-only; 28.08.2026 21:54:15 UTC. Quelle: `/opt/struktur/youtube-research/knowledge.db`, `graphiti_import_queue`, Filter `graphiti_status='done' AND video_id IS NOT NULL`; Neo4j-Quelle Container `graphiti-neo4j`, Label `Episodic`, vollständiger Property-Join. - Queue-`done`: 820 Zeilen / 630 eindeutige IDs - Neo4j-Nachweis: 630 eindeutige IDs - Queue-`done` ohne Neo4j-Nachweis: 0 - Episoden für diese IDs: 1.999 - IDs mit genau einer Episode: 184 - IDs mit mehreren Episoden: 446 Episoden sind nicht Videos; Queue-Zeilen, eindeutige Queue-IDs, Videos und Episoden bleiben getrennte Ebenen. ## 11. Abschluss-Snapshot Endaudit 21:54:15 UTC: | Status | Zeilen | eindeutige IDs | |---|---:|---:| | done | 820 | 630 | | queued | 262 | 253 | | retry_wait | 199 | 195 | | processing | 0 | 0 | | failed_permanent | 0 | 0 | Offene Queue: 461 Zeilen, 448 eindeutige IDs. Videos: 1.098 Zeilen / 1.098 eindeutige YouTube-IDs. Transcriptstatus: `done=1.066`, `partial=18`, `unavailable=13`, `none=1`, `retry=0`. Registry: 1.611 Zeilen / 989 eindeutige `source_external_id`. Graphiti-Endstatus: `/health` 200, `/search` mit `Obsidian` 200 und valides JSON mit `score:null`, `/maintenance/status` 200 mit `maintenance_mode=false`, `write_requests_allowed=true`, `lock_exists=false`, keine aktiven Requests. SQLite: Im AA-047-Beobachtungsfenster keine neuen `database is locked`-/`SQLITE_BUSY`-Meldungen im E2E-Worker-Journal. ## 12. Scheduler-Konzept, nicht aktiviert Der bestehende `obsidian-graphiti-import.timer` verarbeitet eine andere Datenbank und ist kein Queue-Consumer. Der vorhandene `youtube-research-graphiti-worker.service` ist ein `oneshot` mit `--limit 10`, aber seine produktive Aktivierung als Scheduler für den AA-043-Single-Writer ist nicht nachgewiesen und wurde nicht aktiviert. Für einen späteren separaten Auftrag erforderlich: 1. dedizierter systemd-Oneshot-Wrapper, der ausschließlich den freigegebenen Single Writer auf exakt selektierten IDs aufruft; 2. kleine serielle Batchgröße, zunächst `1`, später nur nach Gates begrenzt; 3. atomarer Claim mit Lease/Lock gegen parallele Bearbeitung; 4. `retry_wait` erst nach `next_attempt_at`, Provider-429 mit Backoff/Retry-After und Quota-Block ohne Retry-Sturm; 5. Maintenance-Sentinel als globales Write-Gate respektieren, nicht entfernen; 6. SQLite-Transaktionen kurz halten, `busy_timeout` nutzen und bei Lockfehlern fail-stop statt Timeout-Kaskade; 7. jeder Fehler stoppt den Batch, verifiziert Queue/Request/Episode und startet keinen nächsten Kandidaten; 8. Neustart setzt nur eindeutig nicht beanspruchte bzw. abgelaufene Leases fort; `UNKNOWN_REMOTE_OUTCOME` darf nicht automatisch duplizieren. Kein Timer und kein automatischer Rückstandsabbau wurde aktiviert. ## 13. Abschlussmatrix | Prüffeld | Ergebnis | |---|---| | Maintenance-Sentinel-Erzeuger nachgewiesen | NEIN – Watch ohne Ereignis; konkreter historischer Schreiber nicht nachgewiesen | | Maintenance-Verhalten technisch korrekt | JA | | Providerfehler eindeutig klassifiziert | JA – `RATE_LIMIT` | | Rate-Limit-/Quota-Behandlung korrekt | JA – 429 nach außen, Queue `retry_wait` mit Backoff | | Queue-ID 876 erfolgreich | NEIN – erneuter HTTP 429 | | Pilot 3 erfolgreich | NEIN – nicht gestartet | | Gesamtpilot 3/3 erfolgreich | NEIN | | Doppelwrites | 0 nachgewiesen für Pilot 4; keine Episode für 876 | | `queued` erfolgreich verarbeitet | NEIN | | `retry_wait` erfolgreich verarbeitet | JA – Queue-ID 4, `WRITE_CONFIRMED` | | Queue-`done`→Neo4j fehlend | 0 | | neue SQLite-Lockfehler | 0 | | automatischer Scheduler technisch geklärt | JA – bestehender Timer ungeeignet; Zielkonzept dokumentiert | | automatischer Scheduler aktiviert | **NEIN** | | Massenabarbeitung freigegeben | **NEIN** | | Pipeline vollständig abgeschlossen | **NEIN** | ## 14. Offene Punkte - Der konkrete Prozess, der den historischen Maintenance-Sentinel erzeugte/entfernte, bleibt **Ursache noch nicht nachgewiesen**. Eine längere Überwachung oder kernelbasiertes Audit wäre ein separater Diagnoseauftrag. - Der Provider-Upstream ist weiterhin temporär rate-limited. Provider-/Modellwechsel sind in AA-047 nicht zulässig und wurden nicht vorgenommen. - Ein erfolgreicher Provider-Verfügbarkeitscheck auf `/models` ersetzt keinen erfolgreichen Graphiti-Write. - Die Gesamtpipeline und der offene Queue-Rückstand sind nicht abgeschlossen. Arbeitsauftrag AA-047 erledigt.