segmented history
a room’s history is sections of events with durable gaps between them. the sdk event cache keeps the sections, gap identity, boundaries and each gap’s cursor, encrypted at rest, across room close and restart. macltok shows gaps inline and fills them on request.
opening a room
opening loads retained history and its gaps, then starts a recent load in the background, so opening never waits on the network. the recent load fetches up to four pages of 50 events from a fresh server position. if it reaches the newest retained event the sections join. if it doesn’t, a gap is left between the fresh window and what was retained.
Event::RecentHistoryChanged reports it, one event per edge.
Loadingas it startsLoaded,CancelledorFailed(message)as it settlesIdlewhen the room closes, whatever was happening
Failed stays until HistoryRepairCommand::RetryRecent, and closing the room
turns it to Idle like anything else. a failed load on a room timeline with no
events yet falls back to paginating EngineConfig::initial_history_limit
events, so the room isn’t left empty.
filling a gap
HistoryRepairCommand::FillGap { room_id, gap_id } walks one gap backwards, up
to four pages of 50, each page committing its events and the gap’s new cursor
together. Event::HistoryRepairChanged carries a HistoryRepairReport, once
with phase Bridging as the fill starts and once more as it settles.
Ready, continuityVerified. the older section was reached and the gap is gonePausedwith a stop.Budgetafter four pages,CursorStalledwhen the server repeats a cursor,HistoryUnavailable(continuityAccessibleStart) when accessible history ends first,CancelledPausedwith afailure, and the gap row goesFailed
a gap that is already unavailable answers at once and counts no page. one that
has already joined is gone, so filling it answers EventNotFound.
one operation per room
a room runs one history operation at a time. RetryRecent joins a recent load
that is already running. everything else that overlaps, a fill during a recent
load, a recent load during a fill, a second fill, answers RoomBusy.
HistoryRepairCommand::Cancel stops whatever is running. its own answer is
Ok, the cancelled command answers Interrupted, and the published outcome is
Cancelled, never a failure. pages already committed stay.
the timeline
gaps appear in TimelineState.items as TimelineItem::HistoryGap, between the
sections they separate. a row keeps its id while its lifecycle changes, and is
removed once the gap joins. joined gaps never appear.
history lands as TimelineChanged diffs, never a TimelineReset. they are
incremental unless the cache’s loaded window changed under the operation, after
a restart or a shrink, when the sdk sends the room as one Reset diff. before
answering, the room fences on Timeline::flush_history, which applies what the
cache already queued without replacing the timeline, so receipts, the read
marker and row ids survive. a failed or cancelled operation that committed
pages is fenced the same way.
a command’s answer comes after its outcome is published, and after the mirror has applied it when one is attached.
wire shapes
serde enums are externally tagged.
{ "HistoryRepair": { "FillGap": { "room_id": "!room:example.org", "gap_id": "opaque" } } }
{ "HistoryRepair": { "RetryRecent": { "room_id": "!room:example.org" } } }
{ "HistoryRepair": { "Cancel": { "room_id": "!room:example.org" } } }a gap row.
{
"HistoryGap": {
"id": "opaque",
"older_boundary": "$older:example.org",
"newer_boundary": "$newer:example.org",
"lifecycle": "Idle",
"failure": null
}
}lifecycle is Idle, Paused, Failed or Unavailable. failure is set with
Failed.
a settled fill.
{
"room_id": "!room:example.org",
"gap_id": "opaque",
"pages": 2,
"events": 63,
"phase": "Ready",
"continuity": "Verified",
"stop": null,
"failure": null
}the mirror keeps State.recent_history, a room without an entry being idle,
and State.repairs, the last fill report per room. closing a room drops both.