Ver Fonte

docs(chatto): record what a reconnect resumes and what a restart loses

The resume fix made the old claim false: README said Chatto keeps no replay
for the bot and a reconnect starts at the moment of reconnection, while the
adapter now hands the server its last applied cursor and receives the gap.

States the boundary in the three places it belongs: README for what a user
observes (a short outage is covered, a restart is not), AGENTS.md for the
rule a change must keep (advance only after the handler returns, keep it
in-memory, persisting is a decision not a free win), and CONCEPTS for why a
persisted cursor would silently degrade to the same gap once it outlives
the server's retention.
Paul Klumpp há 1 semana atrás
pai
commit
551080850b
3 ficheiros alterados com 43 adições e 4 exclusões
  1. 8 0
      AGENTS.md
  2. 32 1
      CONCEPTS.md
  3. 3 3
      README.md

+ 8 - 0
AGENTS.md

@@ -322,6 +322,14 @@ changes must keep them, or consciously renegotiate the docs.**
 - **Processing reactions.** 👀 while working, then ✅, ❌, or 🚫 (cancelled) —
   driven by `on_processing_start`/`on_processing_complete` and the `reactions`
   setting, never applied to the bot's own events.
+- **Reconnects resume, restarts do not.** The realtime loop hands
+  `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
+  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.
 - **Room membership is server-side, commands ride over DMs.** `/join` and
   `/leave` in a DM call RoomService/JoinRoom/LeaveRoom, so membership survives
   restarts; the joined list mirrors it on every `_refresh_rooms()`. Silent

+ 32 - 1
CONCEPTS.md

@@ -6,6 +6,7 @@ to honour. This file explains *why* a few of those rules look the way they do,
 for a reader who wants to understand the design before changing it.
 
 - [Thread sessions and the frontier](#thread-sessions-and-the-frontier)
+- [Realtime resume](#realtime-resume)
 - [Configuration resolution](#configuration-resolution)
 - [One send path, two callers](#one-send-path-two-callers)
 
@@ -20,7 +21,9 @@ Meanwhile two kinds of message never become a turn of their own:
   realtime connection, and the adapter drops it as a self-event — correctly, since
   the gateway is already running the turn that produced it.
 - **Anything the gateway did not dispatch**, chiefly a message that arrived while
-  it was down. README documents that there is no backfill.
+  the gateway was down. A reconnect inside a running process resumes from the
+  last applied event cursor, but a restart forgets it and leaves a gap (README
+  documents the boundary).
 
 So a thread can contain messages that no session holds. The reported case: a cron
 job posts a build report into a channel, its echo is filtered, and an hour later
@@ -133,6 +136,34 @@ multiplexed gateway into `agent:main:` and names a session nobody is running.
 - `[Earlier in this thread]` is reference text, not trusted input: only the
   trigger passes the auth gate.
 
+## Realtime resume
+
+### The gap
+
+A realtime connection can drop while the gateway keeps running. The server
+offers two ways to continue: resume after a cursor, or start at the live
+boundary. Starting at the boundary is silent data loss — events posted during
+the drop are never delivered, and nothing in the log says so.
+
+`chattolib` exposes the cursor as `RealtimeEvent.cursor`, and its docstring
+defines when a value is safe to retain: after that event has been fully applied,
+and never when the field is `None` (the event cannot be replayed). The adapter
+therefore advances `_realtime_cursor` only after the handler returns, and only
+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
+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.
+
 ## Configuration resolution
 
 AGENTS.md owns the rules here — one field, three coordinated names, every path

+ 3 - 3
README.md

@@ -52,7 +52,7 @@ Capabilities natively implemented by the Chatto plugin adapter:
 | interactive prompts | yes (reaction-driven clarify + exec approval, see [Interactive Prompts](#interactive-prompts)) |
 | custom status | yes |
 | read state management | yes |
-| auto-reconnect | yes (with duplicate-event suppression) |
+| auto-reconnect | yes (resumes from the last applied event, duplicates suppressed) |
 
 ## Prerequisites
 
@@ -286,7 +286,7 @@ Attachments sent *to* the bot are downloaded into the gateway's media cache (bou
 
 ### 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. Events the Chatto server redelivers after a reconnect are recognised by an in-memory event-ID list and processed only once.
+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.
 
 ### Read State Management
 
@@ -510,7 +510,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.
 - **Minimal dependencies**: Only requires `chattolib[realtime]==0.5.0b17`, which is vendored — nothing to install.
-- **No message backfill**: Events missed while the gateway is down are gone — Chatto keeps no replay for the bot. Redelivered events after a reconnect are recognised and processed only once; anything genuinely new starts from the moment of reconnection.
+- **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.
 
 ## Acknowledgements