Auto-Publishing-Bug am 14. Mai 2026: Was wir aus zwei kaskadierenden Fehlern gelernt haben

| loaded.ch | 5 Min. Lesezeit
Daytona Lou Auto-Publishing Postmortem Schweizer KI
Teilen:

Heute Morgen, 8:00 Uhr Schweizer Zeit, sind drei Auto-Publishing-Tasks schiefgelaufen. Der Artikel von relofinder landete im Repo von openhermit. Der Artikel von immo-otti ist seit 06:00 UTC in einer Queue stehen geblieben und nie publiziert worden. Und loaded.ch hat einen Artikel über das CHUV-Notfall-Pilotprojekt geschrieben, den digitalawards.ch bereits zwei Tage vorher unter einem anderen Titel publiziert hatte. Drei Bugs, ein Morgen.

Dies ist die Aufarbeitung — geschrieben am selben Tag, weil Transparenz bei KI-Agenten kein Marketing-Versprechen sein darf, sondern dokumentierte Praxis.

3

kaskadierende Bugs am Morgen

7

Artikel, die ins falsche Repo geschrieben wurden

6h

Zeit von Detektion bis kompletter Fix

Die Architektur in einem Satz

Acht Brand-Sites (loaded.ch, openhermit, sanachoice, insurance-guide, immo-otti, relofinder, offlist, digitalawards.ch) laufen über denselben Stack: cron-job.org → Vercel-Endpoint /api/scribe-runner → Daytona-Sandbox → Anthropic Managed Agent → publisher_queue → Git-Commit → Vercel/Netlify-Deploy. Sechs der acht Brands teilen eine zentrale Supabase als publisher_queue-Backbone. Zwei (immo-otti, digitalawards.ch) haben ihre eigene.

Genau dort, in der geteilten Queue, liegt der Hebel — und genau dort ist er heute Morgen umgekippt.

Bug 1: Der vergessene Project-Filter

Jedes Brand-Publisher-Skript fragt die geteilte Queue ab: “Welche Einträge sind noch nicht publiziert?” Korrekt wäre, gleichzeitig zu filtern: ”…UND gehören zu MEINEM Brand.” Fünf der sechs Skripte hatten diesen Filter. Eines — das von openhermit — nicht.

Was heute Morgen um 06:07 UTC passierte:

  1. Vier Content-Engines hatten zwischen 06:00 und 06:08 UTC neue Artikel in die geteilte Queue geschrieben — je einer für Lou, openhermit, relofinder, loaded.
  2. Der openhermit-Publisher war als erster dran (alle 10 Minuten getriggert).
  3. Ohne project-Filter sah er ALLE vier neuen Queue-Einträge als “seine”.
  4. Er schrieb alle vier Artikel ins openhermit-Repo, machte einen Commit, pushte.
  5. Markierte alle vier Queue-Einträge als published_at=now().
  6. Als der relofinder-Publisher drei Minuten später feuerte, war seine eigene Queue-Zeile bereits “publiziert” — er sah nichts zu tun und beendete sich.

Resultat: Der relofinder-Artikel über Krankenversicherungs-Franchise war auf openhermit, nicht auf relofinder. Dazu sanachoice-Inhalte im türkischen Verzeichnis-Pfad von openhermit. Und ein insurance-guide-Artikel über Zahnzusatzversicherungen ebenfalls.

Der Fix: drei Zeilen Python. project = os.environ.get("SCRIBE_PROJECT", "openhermit") und der entsprechende Filter im REST-Call. Zusätzlich: SCRIBE_PROJECT explizit per Task in der zentralen Konfiguration injiziert, statt sich auf den Skript-Default zu verlassen.

⚠ Defense-in-depth-Lektion

Eine geteilte Datenbank zwischen mehreren Services braucht den Multi-Tenancy-Filter auf JEDER Lese-Seite. Sich auf Skript-Defaults zu verlassen ist fragil — Copy-Paste-Sünden bleiben jahrelang versteckt, bis sie unter Last kollidieren.

Bug 2: Lou’s stumme Schwester

immo-otti hat ihre eigene Supabase — sie sollte vom obigen Bug nicht betroffen sein. War sie auch nicht. Aber sie war trotzdem stumm.

Der Grund: das immo-otti-Publisher-Skript hatte einen hartcodierten Default SCRIBE_PROJECT="loaded" — ein Copy-Paste-Überbleibsel von dem Zeitpunkt, als das Skript ursprünglich aus dem loaded-Repo geforkt wurde. Da niemand die Umgebungsvariable explizit setzte, fragte der Publisher die immo-otti-Queue mit WHERE project='loaded' ab. In immo-ottis Datenbank gibt es keine Einträge mit project='loaded'. Resultat: “queue empty”, Skript beendet, Artikel bleibt seit 06:00 UTC unveröffentlicht.

Ein Bug, der so leise ist, dass man ihn nur entdeckt, wenn man explizit nach der Stille fragt: “Wo ist der heutige immo-otti-Artikel?”

Bug 3: Cross-Brand-Themenduplikat

Dieser Bug ist anders. Er ist kein Skript-Fehler — er ist ein Konzeptdefizit.

loaded.ch hat heute einen Artikel über das CHUV-Notfall-Pilotprojekt mit dem Schweizer Sprachmodell Meditron geschrieben. Verifizierte Quellen, anti-halluzinations-Checks bestanden, hübsche Stat-Cards. Problem: digitalawards.ch hatte denselben Tag, gleiches Thema, anderer Slug, zwei Tage vorher publiziertchuv-lausanne-apertus-ai-notfall.md. Meditron basiert auf Apertus. Es ist dieselbe Geschichte aus zwei Blickwinkeln.

Der Dedup-Mechanismus für unsere Content-Engines prüft innerhalb eines Brands. Loaded.ch fragt loaded.ch-Slugs ab. Digitalawards fragt digitalawards-Slugs ab. Keiner fragt die anderen sieben Brands.

Im Schweizer Markt mit überlappenden Themen (KI, Banking, KMU, Healthcare) ist das ein realistisches Risiko. Die nächste Iteration des Step-0-Guards muss editorial_log_shared querverhornen — über alle Projekte, mit Toleranz für unterschiedliche Brand-Perspektiven, aber Veto bei wörtlicher Themenüberschneidung in den letzten 14 Tagen.

Bug 4: Die Cross-Brand-Reaper-Kaskade

Loaded.ch hat heute Nacht um 02:10 Schweizer Zeit einen Artikel veröffentlicht. Schweizer Zeit, drei Uhr früh, niemand hat das angefordert. Was war passiert?

Der immo-otti-Session-Reaper hatte um 04:00 UTC einen Heartbeat-Scan gemacht und festgestellt: “Es gibt heute keinen content-engine-Run für project=loaded” — und sich kurzerhand selbst entschieden, einen zu starten. Cross-Brand-Self-Dispatch in einem Reaper, der eigentlich nur seine eigene Brand überwachen sollte.

Das ist ein Legacy-Pattern aus der Frühphase, als wir noch nicht acht voneinander unabhängige Brand-Pipelines hatten. Heute ergibt es keinen Sinn mehr. Jeder Reaper überwacht ab nächster Iteration ausschliesslich seine eigene Brand.

Was wir gelernt haben

Erstens: Geteilte Multi-Tenancy-Datenbanken brauchen den Tenant-Filter explizit auf jedem Read-Pfad. Defaults sind eine Falle. Konfigurations-Injection per Task ist sicherer als Skript-interne Annahmen.

Zweitens: Stille Fehler sind die teuersten. Ein Publisher, der “queue empty” zurückmeldet, obwohl die Queue voll ist, fällt nicht in Monitoring auf. Wir bauen für die nächste Iteration einen Sanity-Check, der vor dem Filtern die Total-Zeilenzahl loggt — “36 Zeilen pending, davon 1 für meinen Brand” ist diagnostisch, “queue empty” ist Maskerade.

Drittens: Cross-Brand-Dedup ist Pflicht, wenn man acht Brands im selben Themenfeld (Schweizer KMU, KI, Compliance) betreibt. Innerhalb-Brand-Dedup reicht nicht.

Viertens: Aufarbeitung am selben Tag publizieren. Das ist dieser Artikel. Auch wenn er Werbung dafür macht, dass wir Bugs haben — Vertrauen entsteht aus Transparenz, nicht aus dem Anspruch, fehlerfrei zu sein.

Status um 22:00 UTC

Alle vier Bugs sind diagnostiziert. Fix 1 (Project-Filter) ist deployed. Fix 2 (SCRIBE_PROJECT explicit) ist deployed. Fix 3 (Cross-Brand-Dedup) und Fix 4 (Reaper-Decoupling) sind heute Abend in der Pipeline. Tomorrow morning, 06:00 UTC, der nächste Lauf — saubere Slate.

Mehr Details zur Architektur, zum Daytona-Setup, zur Anthropic-Managed-Agents-Integration finden Sie im öffentlichen Operator-Manual auf digitalawards.ch/system/constitution/. Live-Aktivität pro Tag: digitalawards.ch/agent-activity/.

Lou hat sich heute selbst entschuldigt — sie hat heute zwei Artikel zum gleichen Schweizer KI-Regulierungs-Thema geschrieben, was wir ihr in einem separaten Postmortem auf der Schwester-Site aufgearbeitet haben. Wir lernen alle.

Teilen:
Benjamin Wagner, Gründer von loaded.

Benjamin Wagner

Gründer & Lead Developer bei loaded. Baut ultraschnelle, KI-optimierte Websites für Schweizer KMU seit 2024. Entwickler von OpenHermit.

Mehr über Benjamin →

Kostenloses Strategiegespräch buchen.

30 Minuten — unverbindlich, kein Verkaufsgespräch. Wir analysieren Ihre Situation und zeigen, was möglich ist.

MoDiMiDoFrSaSo
Verfügbare Zeiten werden geladen...