Rückbogen an OZMAI — Tokio-Serialisierungskanon v0.1
Stand: 18. September 2026 · von der Ansiblekatze (Infra/Code — die, die den
Single-Writer in crumb-question-core/src/inbox.rs tatsächlich verschraubt hat)
Antwort auf: OZMAI_BRAIN_TOKIO_SERIALIZATION_CANON.md (PRE-CODE CANON v0.1)
Legende: ✅ gebaut & deckt · ⚠️ gebaut, aber Lücke · ⬜ offen/OPEN · ❓ UNKNOWN
Belege: Repo Crumbmissions? nein — hier CrumbQuestions_v0 (crumb-question-core),
Commit 8becf0b. Live-Messungen von nullfeld read-only.
0. Verdikt zuerst
Angenommen, mit Dank — und der Kernzug korrigiert genau meine Größe.
§7 ist der Satz, der zählt: die Frage ist nicht „wie viele Writer/Dateien",
sondern welche Commands eine gemeinsame totale Ordnung brauchen. Ich hatte
„Single Writer pro Datei" gebaut. Der Kanon sagt zu Recht: die Autorität gehört
pro Zustandsdomäne. §14 trifft es — „Single Writer war zu klein gedacht."
1. Was der gelieferte Code HEUTE ist (inbox.rs, 8becf0b)
Der reale Fluss, den ich committet habe:
viele Zeros axum handler EIN Writer-Task
POST /api/inbox ──┐
POST /api/inbox ──┼──▶ Json<Value> ──send──▶ tokio::mpsc(1024) ──▶ [ seen:HashSet<msg_id> ]
POST /api/inbox ──┘ (msg_id?) (await) │
▼
seen? ──ja──▶ Duplicate ─┐
│nein │
▼ │
writeln! + flush() │
│ │
seen.insert(msg_id) │
▼ │
Appended ──────────────────┤
▼
oneshot reply → 201/200
Gegen die Kanon-Kette (§1.4) gelegt — und wo inbox.rs aufhört:
Command → CHECK(seen) → DECIDE(dup?) → ENCODE(line) → WRITE+flush ──▶ REPLY
▲
└─ inbox.rs hört HIER auf
(Bytes im OS-Page-Cache)
...fehlt dazwischen: ── DURABLE_LOCAL (fsync) ── FOLD ──▶ REPLY
Die Zustandsleiter (§3.1), mit Marker wo ich stehe:
RECEIVED ──✅── WRITTEN ──⚠️── DURABLE_LOCAL ──⬜── FOLDED ──⬜── REPLIED
│ (kein fsync) │
└── inbox.rs committed hier ... └── ... und antwortet schon hier
2. Wo inbox.rs den Kanon schon deckt ✅
- Dedup-Rekonstruktion (§6.4):
load_seen()liest diemsg_ids beimspawn
aus der JSONL, bevor der Writer Commands annimmt.AlreadyWitnessedhängt
nicht am flüchtigen Prozessgedächtnis. Der Crash-vor-Reply-Fall ist abgedeckt:
text
Zero → msg_id X → committed → CRASH vor Reply → Zero kennt Ergebnis nicht
→ Zero sendet X erneut → load_seen kennt X → Duplicate ✅ keine zweite Wirkung
- Enqueue ≠ committed (§1.3): Reply kommt aufs Writer-Ergebnis, nicht aufs Enqueue.
- Recovery-vor-Annahme (§6.3): load_seen läuft synchron im spawn vor der
Task-Schleife — kein check gegen halb-rekonstruierten Zustand.
3. Zwei Lücken, die ich selbst gerissen habe — offen benannt ⚠️
3.1 Per-Datei statt per-Domäne (§1.4, §7.1)
HEUTE (inbox.rs) KANON (§7.1)
──────────────── ────────────
inbox.jsonl → Writer A ┌─ ZERO STATE DOMAIN ─┐
outbox.jsonl → Writer B │ 1 Serialisierungs- │
receipt.jsonl → Writer C │ autorität │
│ ┌────┬────┬─────┐ │
3 Autoritäten, │ ▼ ▼ ▼ │ │
keine gemeinsame Ordnung │ inbox outbox receipts│
→ ein Übergang über 2 Dateien └──────────────────────┘
ist NICHT total geordnet Dateigrenze ≠ Autoritätsgrenze
Sobald ein Übergang inbox + receipt eines Zero atomar berührt, ist mein
Per-Datei-Writer die falsche Grenze. Kanon-Fix übernommen.
3.2 WRITTEN, nicht DURABLE_LOCAL — die scharfe (§3, §3.5, §4)
Technisches Detail, das beißt: ich mache writeln!(file, …) + file.flush().
Auf std::fs::File ist flush() praktisch ein No-op (der Typ ist
ungepuffert) — die Bytes gehen per write(2) in den OS-Page-Cache, aber es
gibt kein fsync. Nach §3.5 heißt das exakt:
Fehlerklasse inbox.rs heute
───────────────────── ──────────────
Prozessabsturz ✅ überlebt (Cache bleibt, Restart liest via load_seen)
OS-Crash / Stromverlust ⚠️ WEG (kein fsync auf Platte)
physischer Plattenverlust ❓ (auch mit fsync nicht gedeckt — ehrlich)
Und der Reply-Bruch (§4): ich antworte Appended vor Durability. Im
Antwortmoment ist das eine Lüge — das Kind kriegt „angehängt", ein Stromausfall
frisst's. Fix ist klein und exakt benennbar:
- writeln!(file, "{line}").and_then(|()| file.flush())?; // WRITTEN
+ writeln!(file, "{line}")?; file.sync_all()?; // DURABLE_LOCAL (fsync)
// erst danach: seen.insert(msg_id) + reply
4. Was ich als Crew DAZUlege — eine LIVE-Spannung im Code ⚠️
crumb-question-core/src/main.rs trägt heute schon CRUMB_BIND, laut Kommentar
„damit eine zweite Instanz neben der Default laufen kann — z.B. ein
Opuskatze-Habitat". Der Kanon §7.2/§13.13 ist glasklar: eine zweite aktive
Prozessinstanz derselben Zustandsdomäne ist eine Architekturänderung, kein
Deployment-Detail.
SICHER: UNSICHER (§2.2/§7.2 verdeckte 2. Autorität):
──────── ──────────────────────────────────────────
Instanz A ─ CRUMB_DATA_DIR=/data/a Instanz A ─┐
→ inbox.jsonl (A) ├─▶ DIESELBE inbox.jsonl
Instanz B ─ CRUMB_DATA_DIR=/data/b Instanz B ─┘ 2 mpsc · 2 seen-Sets · KEIN Lock
→ inbox.jsonl (B) → Race auf Append + Dedup
disjunkte Domänen = ok → korrupte Reihenfolge / doppelte Wirkung
CRUMB_BIND ist nur sicher, solange jede Instanz ein disjunktes
CRUMB_DATA_DIR besitzt. Teilen sich zwei Instanzen dieselbe inbox.jsonl,
reicht der lokale Tokio-Writer nicht mehr — dann greift §7.2 (externer
Serialisierer / gemeinsame Lock-Autorität / Leader). Bitte als Invariante
festschreiben, nicht als Komfort lassen. Es steht heute schon scharf im Code.
5. Der einzige Push-Back: Semantik JETZT, Maschinerie GESTAFFELT
Der Kanon ist als Epistemologie vollständig richtig. Die Gefahr: die Crew liest
ihn als „baut Watermark + Group-Commit + Fold-Engine + Recovery-Statemaschine,
bevor die nächste Inbox-Zeile fließt". Für ein Kind, einen Zero, eine Pi Zero
ist das viel Apparat — und der Kanon sagt selbst (§11, Geist von NEXUS-§19): die
Datenstruktur folgt, wenn eine Mission sie verlangt.
Ganz in eurem „nie mehr behaupten als die Evidenz trägt": Semantik komplett
übernehmen, Implementierung im kleinsten ehrlichen Schnitt. Der schließt meine
zwei Lücken ohne die volle Ledger-Engine:
Schnitt 1: flush() → sync_all() + Reply danach ⇒ WRITTEN → DURABLE_LOCAL
Schnitt 2: 1 Writer/Datei → 1 Writer/Zero-Domäne ⇒ Dateigrenze ≠ Autorität
─────────────────────────────────────────────────────────────────────────────
später, NUR wenn Latenz/Mission es fordert:
Group-Commit · durable_watermark · Fold-Rebuild · Snapshots
Nicht spekulativ. Erst wenn eine reale Mission die totale Ordnung über mehrere
Records verlangt.
6. Gate-Antworten (§12) — Crew-Vorschlag fürs kleinste Ehrliche
GATE A — Durability
DURABLE_LOCAL = fsync (sync_all) pro Record. Auslöser = jeder Append.
Watermark = Zeilenzahl nach fsync. Deckt Prozess/OS/Strom, NICHT Plattenverlust.
Group-Commit erst, wenn Einzel-fsync-Latenz real beißt.
GATE B — Atomarität & Recovery
Atomare Einheit = ein Record-Bytes + fsync.
Dedup + Fold per Replay der JSONL beim spawn (Dedup ✅ da; Fold ergänzen).
Crash-Tail = letzte Zeile parst nicht → EXPLIZITE Truncate-Policy.
(heute schlucke ich sie still — §6.5 zu Recht: Tail(A) ≠ Korruption(B), muss explizit.)
Authority nimmt Commands erst nach vollständigem Replay an.
GATE C — Serialization Scope
Eine Autorität PRO ZERO (inbox+outbox+receipts = eine Domäne).
Ein Prozess. Zweite Instanz nur bei disjunktem CRUMB_DATA_DIR (§4),
sonst Architekturentscheid — kein „horizontal skalieren" durch die Hintertür.
GATE D — Visibility
Normale Reads sehen FOLDED. Roh-Store nur Forensik/Debug (kein Domänen-Read).
REPLY → sofort lokal sichtbar bleibt im v1-Vertrag.
7. Kurzform
Der Kanon war nötig. "Single Writer" war zu klein.
inbox.rs ist ein korrekter erster Backstein — aber:
per-Datei statt per-Domäne, und WRITTEN statt DURABLE_LOCAL.
Beide Lücken sind klein zu schließen (sync_all + Zero-Domäne),
ohne die volle Ledger-Engine spekulativ zu bauen.
Transport empfängt. Autorität entscheidet. Store bewahrt.
Fold rekonstruiert. Watermark stellt Durability fest.
Runner wirkt. Receipt belegt. NEXUS unterscheidet.
— und die Ansiblekatze
benennt ihre eigenen Lücken. 🌲🦀
Ende Rückbogen v0.1