Explorer
/tmp/aa044_report.py
← Zurück ↓ Download
import sqlite3,os,datetime,subprocess,json,hashlib
DB='/opt/struktur/youtube-research/knowledge.db'; VAULT='/opt/obsidian-vault/YouTube-Research'; OUT='/opt/struktur/reports/AA-044.md'
c=sqlite3.connect('file:'+DB+'?mode=ro',uri=True); c.row_factory=sqlite3.Row
rows=[dict(x) for x in c.execute('''select q.id queue_id,q.video_id,q.obsidian_path,q.graphiti_status,q.graphiti_attempts,q.attempt_count,q.graphiti_last_attempt_at,q.last_attempt_at,q.next_attempt_at,q.last_error_class,q.last_error_message,q.last_http_status,q.created_at,q.updated_at,q.graphiti_processed_at,q.completed_at,q.graphiti_release,q.locked_at,q.lease_expires_at,v.title,v.transcript_status,length(v.transcript) transcript_len,v.transcript_last_attempt from graphiti_import_queue q left join videos v on v.youtube_id=q.video_id where q.graphiti_status in ("queued","retry_wait") order by q.id''')]
status=c.execute('select graphiti_status,count(*) n,count(distinct video_id) u from graphiti_import_queue group by graphiti_status order by graphiti_status').fetchall(); vids=c.execute('select count(*) n,count(distinct youtube_id) u from videos').fetchone(); tr=c.execute('select coalesce(transcript_status,"<NULL>") s,count(*) n from videos group by transcript_status order by s').fetchall(); reg=c.execute('select coalesce(graphiti_status,"<NULL>") s,count(*) n from source_processing_registry group by graphiti_status order by s').fetchall(); c.close()
md=sum(1 for root,ds,fs in os.walk(VAULT) for f in fs if f.endswith('.md')); now=datetime.datetime.now(datetime.timezone.utc).isoformat(); from collections import Counter
ids={r['video_id'] for r in rows}; dup={k:v for k,v in Counter(r['video_id'] for r in rows).items() if v>1}; due=sum(bool(r['next_attempt_at'] and r['next_attempt_at']<=now) for r in rows); nodue=sum(not r['next_attempt_at'] for r in rows); future=sum(bool(r['next_attempt_at'] and r['next_attempt_at']>now) for r in rows)
def sh(cmd): return subprocess.run(cmd,shell=True,text=True,capture_output=True,timeout=40).stdout.strip()
health=sh("curl -sS -m 20 http://127.0.0.1:8644/health"); search=sh("curl -sS -m 30 -X POST http://127.0.0.1:8644/search -H 'Content-Type: application/json' -d '{\"query\":\"Obsidian\",\"limit\":1}' -w '\\nHTTP %{http_code}'")
L=['# AA-044 – Read-only Diagnose YouTube-Research → Queue → Obsidian → Neo4j/Graphiti','',f'Prüfzeitpunkt: {now} (UTC)','', '## 1. Executive Summary','',f'Pipeline abgeschlossen: **NEIN**. Die produktive Queue hat {len(rows)} offene Zeilen und {len(ids)} eindeutige offene Video-IDs. Der aktive Fünf-Minuten-Timer verarbeitet eine getrennte Datenbank (`/opt/struktur/obsidian-graphiti-import/obsidian_graphiti_import.db`, Tabelle `files`) und ist daher kein nachgewiesener Verbraucher der produktiven `knowledge.db.graphiti_import_queue`.','', '**Nachgewiesene Graphiti-500-Ursache:** `/app/main.py:766` serialisiert jedes Suchresultat mit `r.score`; ein zurückgegebenes `EntityEdge` besitzt dieses Attribut nicht.','', 'Keine Queue-, Status-, Service-, Container- oder Datenbankänderung wurde ausgeführt.','', '## 2. Verifizierter Ist-Zustand','',f'- `videos`: {vids[0]} Zeilen, {vids[1]} eindeutige YouTube-IDs.',f'- Transcript: '+', '.join(f'`{r[0]}={r[1]}`' for r in tr)+'.',f'- Queue: '+', '.join(f'`{r[0]}={r[1]} Zeilen/{r[2]} IDs`' for r in status)+'.',f'- Obsidian: {md} Markdown-Dateien unter `{VAULT}`.',f'- Registry: '+', '.join(f'`{r[0]}={r[1]}`' for r in reg)+'.','- Vollständiger Queue→Neo4j-Audit `queue-done-neo4j-complete-property-join-v2`: 629 eindeutige Queue-`done`-IDs nachgewiesen, 0 ohne Nachweis, 1.982 Episoden für diese IDs.','', '## 3. Queue-Analyse','',f'- Offene Queue: {len(rows)} Zeilen / {len(ids)} eindeutige IDs.',f'- Mehrfach vorhandene offene Video-IDs: {len(dup)}; zusätzliche Duplikatzeilen: {sum(n-1 for n in dup.values())}.',f'- Retry-Zeitpunkt überschritten: {due}; noch nicht erreicht: {future}; ohne Retry-Zeitpunkt: {nodue}.',f'- Mit gespeichertem Fehler: {sum(bool(r["last_error_class"] or r["last_error_message"]) for r in rows)}; ohne Fehler: {sum(not (r["last_error_class"] or r["last_error_message"]) for r in rows)}.','', '| Status / Transcript | Zeilen | eindeutige IDs |','|---|---:|---:|']
ct=Counter((r['graphiti_status'],r['transcript_status'] or '<NULL>') for r in rows)
for k,n in sorted(ct.items()): L.append(f'| `{k[0]}` / `{k[1]}` | {n} | {len({r["video_id"] for r in rows if (r["graphiti_status"],r["transcript_status"] or "<NULL>")==k})} |')
L += ['', '### Vollständige offene Queue-Tabelle','', '| Queue-ID | Video-ID | Status | Titel | Transcript-Länge | Versuche | letzter Versuch | nächster Retry | Fehlerklasse | HTTP | Obsidian vorhanden |','|---:|---|---|---|---:|---:|---|---|---|---:|---|']
for r in rows:
 p=r['obsidian_path'] or ''; fp=p if p.startswith('/') else os.path.join(VAULT,p); ex=os.path.isfile(fp); err=(r['last_error_class'] or r['last_error_message'] or '').replace('|','/').replace('\n',' '); title=(r['title'] or '').replace('|','/').replace('\n',' ')
 L.append('| '+ ' | '.join(map(str,[r['queue_id'],r['video_id'] or '',r['graphiti_status'],title,r['transcript_len'] or 0,r['graphiti_attempts'] or r['attempt_count'] or 0,r['last_attempt_at'] or r['graphiti_last_attempt_at'] or '',r['next_attempt_at'] or '',err,r['last_http_status'] or '',('ja' if ex else 'nein')+' `'+p+'`']))+' |')
L += ['', '## 4. Worker, Scheduler und Ursache des Stillstands','', '- `youtube-research-e2e-worker.service`: `active/running`, MainPID 2755892; Journal belegt wiederholte Abbrüche mit `sqlite3.OperationalError: database is locked` bei `e2e_worker.py:60`, `BEGIN IMMEDIATE`.', '- `obsidian-graphiti-import.timer`: `active/waiting`, `OnCalendar=*:0/5`; der Dienst läuft mit `--once --limit 1` auf der separaten `obsidian_graphiti_import.db`.', '- Importer-Auswahl: `select * from files where status in ("queued","retry") and attempts<? order by id limit ?`; das ist nicht `knowledge.db.graphiti_import_queue`.', '- `youtube-research-graphiti-worker.service` ist als oneshot definiert, wurde aber nicht als laufender Worker nachgewiesen.', '- Nachweis: Ein laufender Verbraucher der produktiven Queue ist **nicht nachgewiesen**. Ob die SQLite-Locks die alleinige Ursache der offenen Graphiti-Queue sind: **Ursache noch nicht nachgewiesen**.','', '## 5. Graphiti `/search`','', '- Request: `POST http://127.0.0.1:8644/search` mit `{"query":"Obsidian","limit":1}`.',f'- Response: {search}.','- Traceback: `AttributeError: EntityEdge object has no attribute score`.', '- Code: `/app/main.py:766`: `{"results": [{"fact": r.fact, "score": r.score} for r in results]}`.', '- Health: `/health` HTTP 200 mit `neo4j_ready=true`, `provider_ready=true`, `provider_authenticated=true`, `active_request_count=0`.', '- Kategorie: Graphiti-interner Response-Serialisierungsfehler; kein Request-Schema-, Provider-, Modell- oder Neo4j-Verbindungsfehler nachgewiesen.','', '## 6. Konsistenzabgleich','', '| Ebene | Wert | Bedeutung |','|---|---:|---|',f'| Videos / eindeutige IDs | {vids[0]} / {vids[1]} | Source-Ebene |',f'| Queue gesamt | {sum(r[1] for r in status)} Zeilen | Queue-Ebene |',f'| Queue eindeutige IDs | {sum(r[2] for r in status)} | Duplikate möglich |',f'| Queue done | {next((r[1] for r in status if r[0]=="done"),0)} Zeilen | nicht gleich Videos |','| Neo4j Queue-done IDs | 629 | Auditwert, eindeutige IDs |','| Episoden für Queue-done IDs | 1.982 | nicht gleich Videos |',f'| Obsidian Markdown | {md} | Dateien, nicht eindeutige Videos |','', 'Die unterschiedlichen Ebenen sind nicht direkt gleichsetzbar. Die offene Queue ist nicht durch Episodenzahlen oder Obsidian-Dateien erklärt. Ein vollständiger offener-ID-zu-Neo4j-Audit liegt für die offenen IDs nicht vor; daher ist die offene Neo4j-Konsistenz **nicht vollständig nachgewiesen**.','', '## 7. Nicht nachweisbare Punkte','', '- Die vollständige Kausalität zwischen dem produktiven Queue-Rückstand und den SQLite-Locks ist nicht nachgewiesen.', '- Die genaue Trigger-/Runtime-Verbindung zwischen `aa043_single_writer.py` und dem aktiven Systemd-/Timer-Bestand ist nicht vollständig nachgewiesen.', '- Ein einfacher Graphiti-Healthcheck beweist keine funktionierende Suche; der Search-Test ist weiterhin HTTP 500.','', '## 8. Reparaturvorschläge – nicht ausgeführt','', '1. Den tatsächlichen produktiven Queue-Consumer eindeutig auf `knowledge.db.graphiti_import_queue` abbilden und einen kontrollierten Pilot-Dry-Run vorbereiten.', '2. Die konkurrierenden SQLite-Writer und Transaktionsgrenzen untersuchen und den Lock-Verursacher isolieren.', '3. In `/app/main.py` `score` nur bei vorhandenem Attribut serialisieren; danach Search-POST und Log erneut prüfen.', '4. Für offene IDs einen eigenen vollständigen Queue→Neo4j-ID-Audit durchführen.','', '## 9. Abschlussmatrix','', '| Prüffeld | Ergebnis |','|---|---|','| Pipeline abgeschlossen | **NEIN** |','| Queue funktionsfähig | **NICHT NACHGEWIESEN** |','| Graphiti-Ingestion funktionsfähig | **NICHT VOLLSTÄNDIG NACHGEWIESEN** |','| Graphiti-Search funktionsfähig | **NEIN** |','| Neo4j-Datenbestand konsistent | **NICHT VOLLSTÄNDIG NACHGEWIESEN** |','| Obsidian-Datenbestand konsistent | **NICHT VOLLSTÄNDIG NACHGEWIESEN** |','', '## 10. Reproduzierbarkeit','', 'Read-only verwendet: Python/SQLite `mode=ro` auf `/opt/struktur/youtube-research/knowledge.db`; direkte Dateisystemzählung unter `/opt/obsidian-vault/YouTube-Research`; `systemctl`, `journalctl`, `docker ps`, `curl`; Audit `python3 /opt/struktur/graphiti/youtube_queue_neo4j_audit.py`.','', '**Arbeitsauftrag AA-044 Diagnosebericht erstellt; keine Reparatur ausgeführt.**']
open(OUT,'w').write('\n'.join(L)+'\n'); print(json.dumps({'path':OUT,'bytes':os.path.getsize(OUT),'sha256':hashlib.sha256(open(OUT,'rb').read()).hexdigest(),'open_rows':len(rows),'open_ids':len(ids)},ensure_ascii=False))