Files
ng-eventually/docs/incidents/2026-07-14-write-loss-on-disconnect.md
T
Sylvain Duchesne 0d52c82ba9 docs: passer vision, readcap-and-nuri-model et l'incident en anglais
Le reste du dossier docs/ était déjà en anglais ; ces trois fichiers avaient été
rédigés en français par erreur. Traduction fidèle, sans changement de fond :
mêmes sections, mêmes tableaux, mêmes blocs de code. Le retour à la ligne dur à
78 colonnes est levé (une ligne par paragraphe, convention du projet).

Marqueurs épistémiques préservés et rendus aussi visibles : VERIFIED / INFERRED /
CORRECTED / DIRECTION / GAP. Les citations verbatim de commentaires amont restent
intactes.

Deux incohérences de FOND signalées par la traduction et corrigées ici — elles
étaient invisibles tant qu'on lisait chaque section isolément :

- readcap-and-nuri-model, section « Caveats / gaps » : elle listait encore le
  fetch keyless comme hypothèse INFÉRÉE à confirmer, alors que le bloc CORRIGÉ du
  §4bis la déclare fausse et non constructible. Contradiction interne née de ma
  correction partielle. Conservée barrée plutôt que supprimée : l'hypothèse est
  intuitive et se reformera sinon.
- incident write-loss : l'intro affirmait en fait établi que « l'écriture
  n'atteint jamais durablement le broker », alors que la réserve épistémique plus
  bas dit explicitement que l'alternative (perte d'écriture vs réhydratation à
  froid) n'est pas tranchée. L'intro ne rapporte plus que le symptôme observé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-28 15:51:22 +02:00

5.6 KiB

Write loss on socket death (SerializationError)

Post-mortem — 2026-07-14 · Status: OPEN (not addressed).

An entity written just before a period of inactivity can be silently lost: it is absent on reconnection. (Whether the write never durably reached the broker, or reached it and is not read back on a cold reconnection, is not settled — see Epistemic caveat below. The wording here deliberately states only the observed symptom.) The account / identity survives (no fork). Observed in real conditions (Festipod, Firefox) during a pause after login/creation.

Symptom

  1. The user logs in, the app creates an entity (a Festipod event).
  2. A period of inactivity follows (idle, tab in the background…).
  3. The broker socket dies spontaneously with SOCKET IS CLOSED Some(Left(SerializationError)).
  4. On reconnection, the created entity has disappeared; the app reads back its own scope empty.

Evidence (VERIFIED — live Firefox logs, verbatim)

… REPLAY TOPIC NOT FOUND <topic> IN OVERLAY <overlay>
… NEED REPLAY true
… SENDING EVENTS FROM OUTBOX RETURNED: Err(TopicNotFound)
[user1][polyfill] resolveAccount(user1) → 1 record          ← the account SURVIVES (no fork)
[user1][polyfill] readScopeIndex(…) → 0 entities            ← but the scope is EMPTY
… set reçu: 0 objets Event (public)
… SOCKET IS CLOSED Some(Left(SerializationError)) [51, 3, 223, …]

Interpretation (plausible mechanism, not settled): the write was pushed into the local outbox, but the socket died before it was durably flushed into the broker topic; on reconnection, the outbox replay fails (Err(TopicNotFound)) because the topic was never created on the broker side → the event is abandoned. The account, for its part, had already been durably resolved (resolveAccount → 1 record): it is neither lost nor forked.

Epistemic caveat. The evidence establishes the symptom (loss + Err(TopicNotFound) + readScopeIndex → 0). The exact mechanism is not settled between (i) loss at write time (the write never durably reaches the broker) and (ii) cold-rehydration failure (the write is on the broker but a fresh session does not reopen its own scope). The Err(TopicNotFound) on the outbox replay leans toward (i) in this Firefox case. See the @data repro below, which exhibits a neighboring symptom but does not settle (i) vs (ii).

Causal chain (TRACED — reading of the NextGraph core, to be re-verified)

  • The SerializationError closes the socket. The core emits the disconnection: broker.rsLocalBrokerMessage::Disconnecteddisconnections_sender.send(...) (≈ broker.rs:1051, to be re-verified — volatile number, navigate by symbol).
  • This disconnection is pushed to subscribers via disconnections_subscribe(cb) (PUSH stream).
  • NextGraph reconnection is an unimplemented // TODO (≈ broker.rs:1051-1076): nothing re-establishes the socket nor re-flushes the outbox.
  • user_connect returns a snapshot { server_id, server_ip, error, since } at call time — not a stream, unusable for detecting a later drop.
  • No write-durability confirmation API: a caller cannot await the guarantee that a write has reached the broker.

What the SDK exposes but does not consume

disconnections_subscribe does fire on this failure — but neither the polyfill (@ng-eventually/client) nor the consumer app subscribes to it. The signal exists, nobody listens to it; on the app side, no mechanism retries or warns the user.

Scope & not reproduced

  • Observed on Firefox only to date. A manual test on another browser did not trigger the SerializationError nor its consequences.
  • @data reproduction (Chromium, real broker) — 2026-07-14, decisive. The existing @data reconnection test (reconnexion-meme-identite) was a false green: it read A's repos back from the persistent profile's local IndexedDB, never from the broker. A genuinely cold reader (non-persistent freshBrowser context, the same wallet/account A, no local state — seeded from the wallet captured before the event) reads 0 events from A (BARRIER timed-out (8000ms), CONNECTION ESTABLISHED). A different signature from the Firefox case (no socket death; the OUTBOX empty is the reader's, trivially empty) and it does not settle (i) vs (ii) — an empty barrier is compatible with both. Established on the other hand: @data has never verified the broker durability of A's own reads, and cold rehydration from the broker fails. Repro: src/modules/event/features/reconnexion-froide-sans-local.feature (Festipod).
  • To settle (i) vs (ii): independently verify that A's write reaches the broker — e.g. a warm reader / a second identity reads the event's public doc (the two-identity isolation scenario). If it sees it → the write is durable → the cold reader's 0 is a (ii) (rehydration). Otherwise → (i).

Fix leads (not arbitrated)

  1. Core — fix the SerializationError and implement the reconnection TODO (re-establish the socket + re-flush the outbox).
  2. SDK / polyfill — consume disconnections_subscribe → reconnection + outbox re-flush as a mitigation, independently of the core.
  3. Durability API — expose a confirmation that a write has reached the broker, so that the caller can await it.
  • docs/nextgraph-current-state.md — current state of the core (disconnection / reconnection to be cross-referenced here).
  • Product impact + consumer-side caveat: Festipod concept data-layercaveat_write-durability-across-disconnect.