Explorer
/opt/struktur/reports/AA-047.md
← Zurück ↓ Download
# 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.