Bläddra i källkod

docs(chatto): describe the restart resume and the window that bounds it

Replaces the 'a restart is never replayed' promise with what now ships:
a clean stop stores the position, a restart inside CHATTO_RESUME_WINDOW
receives the gap, and a crash or an expired position falls back to the
live boundary. CONCEPTS records the three bounds as decisions (shutdown
writes, freshness window, profile scope) rather than leaving them to be
inferred from the code.
Paul Klumpp 1 vecka sedan
förälder
incheckning
ff8e8218ff
3 ändrade filer med 39 tillägg och 17 borttagningar
  1. 9 5
      AGENTS.md
  2. 27 10
      CONCEPTS.md
  3. 3 2
      README.md

+ 9 - 5
AGENTS.md

@@ -324,14 +324,18 @@ changes must keep them, or consciously renegotiate the docs.**
 - **Processing reactions.** 👀 while working, then ✅, ❌, or 🚫 (cancelled) —
 - **Processing reactions.** 👀 while working, then ✅, ❌, or 🚫 (cancelled) —
   driven by `on_processing_start`/`on_processing_complete` and the `reactions`
   driven by `on_processing_start`/`on_processing_complete` and the `reactions`
   setting, never applied to the bot's own events.
   setting, never applied to the bot's own events.
-- **Reconnects resume, restarts do not.** The realtime loop hands
+- **Reconnects resume, restarts resume inside a window.** The realtime loop hands
   `stream_events` the cursor of the last event it finished handling, and advances
   `stream_events` the cursor of the last event it finished handling, and advances
   it only after the handler returns: a raising handler leaves the old cursor so
   it only after the handler returns: a raising handler leaves the old cursor so
   the event is redelivered, and an unreplayable event (`cursor is None`) leaves
   the event is redelivered, and an unreplayable event (`cursor is None`) leaves
-  the previous one intact. `_seen` still suppresses duplicates. The cursor is
-  in-memory on purpose — a gateway restart has no position to resume from, so a
-  gap across a restart is not replayed. Persisting it is a product decision
-  about retention and fallback, not a free win; do not add it silently.
+  the previous one intact. `_seen` still suppresses duplicates. A clean
+  `disconnect()` stores the cursor under the profile's home (`state/`), keyed by
+  `_profile_scope_key()`, and `connect()` restores it — but only while it is
+  younger than `CHATTO_RESUME_WINDOW` (default 900 s) and only for the profile
+  that wrote it. A crash stores nothing, and an expired or foreign cursor is
+  refused rather than trusted, so the stream falls back to the live boundary.
+  Do not write on every event: the file is written at shutdown on purpose, and
+  making it per-event needs a throttling decision recorded here first.
 - **Room membership is server-side, commands ride over DMs.** `/join` and
 - **Room membership is server-side, commands ride over DMs.** `/join` and
   `/leave` in a DM call RoomService/JoinRoom/LeaveRoom, so membership survives
   `/leave` in a DM call RoomService/JoinRoom/LeaveRoom, so membership survives
   restarts; the joined list mirrors it on every `_refresh_rooms()`. Silent
   restarts; the joined list mirrors it on every `_refresh_rooms()`. Silent

+ 27 - 10
CONCEPTS.md

@@ -154,16 +154,33 @@ when the event carried one. Two behaviours follow from that placement rather
 than from extra code: a handler that raises leaves the old cursor in place, so
 than from extra code: a handler that raises leaves the old cursor in place, so
 the event is redelivered, and an unreplayable event cannot wipe a good position.
 the event is redelivered, and an unreplayable event cannot wipe a good position.
 
 
-### Why a restart still leaves a gap
-
-Persisting the cursor would extend the resume across process restarts, and the
-piece is cheap — one value. What is not cheap is the decision around it: a
-cursor restored from disk may be older than the server's retention, in which
-case the subscription falls back (initialize-from-snapshot or live-only) and the
-"backfill" silently becomes the same gap it was meant to close. That is a
-product decision about how much history to replay and what to tell the user when
-it cannot be, not a detail to smuggle in with a file write. Until it is made
-explicitly, the promise stops at the process boundary, and README says so.
+### What a restart keeps
+
+Persisting the cursor extends the resume across process restarts, and the piece
+is cheap — one value. The cost is in the *bounds*, which is why three of them
+ship with it:
+
+- **Only a clean stop writes.** `disconnect()` stores the last applied cursor;
+  a crash stores nothing, so the gap it leaves is the same gap that existed
+  before. Writing per event instead would buy crash coverage at the price of a
+  file write on every presence change — a trade this plugin does not make
+  silently (AGENTS.md says so, and the file is deliberately written in one
+  place).
+- **Only a fresh position is used.** `CHATTO_RESUME_WINDOW` (default 900 s)
+  decides how much of an outage may be replayed. Without it, a gateway that was
+  down overnight would answer yesterday's messages on startup; with it, an old
+  position is discarded and the stream starts at the live boundary. That is
+  also what the server would do anyway — chattolib requests
+  `INITIAL_STATE_LIVE_ONLY`, so a cursor it cannot resume degrades to the live
+  boundary rather than failing.
+- **Only the profile that wrote it reads it.** The file lives under the
+  profile's own home and carries `_profile_scope_key()`, the same key the
+  configuration cache and the platform registry use. Under multiplexing, a
+  position written by one profile must never be spent on another profile's
+  stream — the events differ even when the server is the same.
+
+A stored value that fails any of these checks is refused, not repaired: the
+adapter then behaves exactly as it did before the file existed.
 
 
 ## Configuration resolution
 ## Configuration resolution
 
 

+ 3 - 2
README.md

@@ -182,6 +182,7 @@ platforms:
 | `CHATTO_REACTIONS` | No | `true` | 👀/✅/❌/🚫 processing reactions (`true`/`false`) |
 | `CHATTO_REACTIONS` | No | `true` | 👀/✅/❌/🚫 processing reactions (`true`/`false`) |
 | `CHATTO_EDIT_DISPATCH` | No | `true` | Treat edits of chat messages as corrections: a message being processed restarts with the edited text, an ignored message is re-checked against its new text, already-answered messages stay answered (`true`/`false`) |
 | `CHATTO_EDIT_DISPATCH` | No | `true` | Treat edits of chat messages as corrections: a message being processed restarts with the edited text, an ignored message is re-checked against its new text, already-answered messages stay answered (`true`/`false`) |
 | `CHATTO_EDIT_WINDOW` | No | `300` | How long after posting an edit may still land, in seconds; older edits are ignored |
 | `CHATTO_EDIT_WINDOW` | No | `300` | How long after posting an edit may still land, in seconds; older edits are ignored |
+| `CHATTO_RESUME_WINDOW` | No | `900` | How long a stream position saved by a clean shutdown may still be used after a gateway restart, in seconds. Inside the window the bot receives what arrived while it was down; an older position is discarded. `0` disables it. A crash leaves no position to resume from. |
 
 
 ## Start the Gateway
 ## Start the Gateway
 
 
@@ -288,7 +289,7 @@ Attachments sent *to* the bot are downloaded into the gateway's media cache (bou
 
 
 ### Auto-Reconnect
 ### Auto-Reconnect
 
 
-If the realtime stream drops, the adapter reconnects automatically with exponential backoff (1 s → 30 s max, jittered) and honours server-provided retry hints. It hands the server the cursor of the last event it applied, so delivery picks up where it stopped instead of at the live boundary — messages posted during a short outage still arrive. A redelivered event is recognised by an in-memory event-ID list and processed only once. A gateway restart has no such cursor, so a gap across a restart is not replayed.
+If the realtime stream drops, the adapter reconnects automatically with exponential backoff (1 s → 30 s max, jittered) and honours server-provided retry hints. It hands the server the cursor of the last event it applied, so delivery picks up where it stopped instead of at the live boundary — messages posted during a short outage still arrive. A redelivered event is recognised by an in-memory event-ID list and processed only once. A clean shutdown stores that position, so a restart within `CHATTO_RESUME_WINDOW` (default 900 seconds) also receives what arrived while the gateway was down; a crash stores nothing, and a position older than the window is discarded instead of replaying hours of history.
 
 
 ### Read State Management
 ### Read State Management
 
 
@@ -517,7 +518,7 @@ grep -i "chatto\|websocket\|realtime" ~/.hermes/logs/gateway.log | tail -30
 
 
 - **Self-hosted friendly**: Works with any self-hosted Chatto instance. No cloud account or subscription required.
 - **Self-hosted friendly**: Works with any self-hosted Chatto instance. No cloud account or subscription required.
 - **Minimal dependencies**: Only requires `chattolib[realtime]==0.5.0b17`, which is vendored — nothing to install.
 - **Minimal dependencies**: Only requires `chattolib[realtime]==0.5.0b17`, which is vendored — nothing to install.
-- **Backfill stops at a gateway restart**: while the gateway process runs, a dropped realtime connection resumes from the last event it applied, so messages posted during the gap still arrive. A gateway restart forgets that position, so messages that arrived while it was down are not replayed. Nothing is handled twice: redelivered events are recognised and dropped.
+- **Bounded backfill**: while the gateway process runs, a dropped realtime connection resumes from the last event it applied, so messages posted during the gap still arrive. A clean shutdown stores that position under the profile's home, so a restart within `CHATTO_RESUME_WINDOW` (default 900 seconds) also delivers what arrived while the gateway was down. A crash stores nothing, and a stored position older than the window is discarded rather than replaying hours of history. Nothing is handled twice: redelivered events are recognised and dropped.
 
 
 ## Acknowledgements
 ## Acknowledgements