96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 1 | # macltok design decisions |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 2 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 3 | This document describes the contract implemented in the working tree. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 4 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 5 | ## Engine execution |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 6 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 7 | The engine keeps a contextual task registry. Every spawned task is registered |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 8 | with either its command id and kind, a watcher name, or a room id and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 9 | generation. The run loop continuously joins completed tasks and removes their |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 10 | context. A command-task panic produces its correlated `CommandFailed` event. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 11 | A watcher or current room-actor panic is terminal to the engine. Shutdown aborts |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 12 | and joins the remaining registered tasks. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 13 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 14 | The engine emits `Authenticated`, then `ConnectionChanged(Connecting)`, before |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 15 | starting the sync service. `ConnectionChanged` also represents the later SDK |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 16 | states and the final `Stopped` transition. `Engine::run` returns an |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 17 | `EngineError` for terminal failure and emits `Failed` before `Stopped` when the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 18 | event stream remains available. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 19 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 20 | The driver command channel remains lossless and clients must drain |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 21 | `EventStream` independently of command submission. `ClientHandle` also exposes |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 22 | a `watch` receiver for the current connection state. `RoomsChanged` is an |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 23 | invalidation event. Call `ClientHandle::rooms` for current, normalised room |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 24 | summaries. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 25 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 26 | `ClientHandle` sends `Command` values and receives a `CommandId`. Completion is |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 27 | reported as `CommandSucceeded`, `CommandSucceededWithRoom`, or |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 28 | `CommandFailed`. `CommandFailure` contains the id, outer `CommandKind`, typed |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 29 | `CommandErrorKind`, and display message. Direct request and response methods |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 30 | return `CommandResultError`, which is either a delivered `CommandFailure` or a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 31 | closed reply channel. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 32 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 33 | For timeline sends, success means the SDK send queue accepted the event; |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 34 | network delivery is reported later through timeline send state. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 35 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 36 | `send` reports enqueueing, not completion. Consumers retain the returned id |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 37 | until a terminal result with that id arrives; they match `CommandFailure::kind` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 38 | rather than parsing its display message. The event stream must be drained while |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 39 | commands are submitted, even when several frontend clients share one engine. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 40 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 41 | ## Room lifetime and ordering |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 42 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 43 | An open room has an explicit generation and exactly one of three states. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 44 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 45 | - `Opening` owns the generation and actor request sender |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 46 | - `Open` owns the generation and actor request sender |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 47 | - `Closing` owns the generation and a bounded FIFO of deferred open and close commands |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 48 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 49 | An `Open` command starts an actor immediately and returns its terminal result |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 50 | only after the actor reports readiness. There is no separate readiness waiter or |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 51 | readiness task. Repeated opens while opening are retained by the actor and each |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 52 | gets one terminal result. A close changes the room to `Closing`, rejects work |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 53 | that needs an open actor, and waits for that generation to exit. Opens and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 54 | closes during closing are queued in order; after exit, each deferred command is |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 55 | replayed, with a fresh generation started when needed. Events from earlier |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 56 | generations are ignored. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 57 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 58 | Each room actor serialises its timeline work in FIFO order. Different rooms run |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 59 | concurrently. Room mutations which do not require an open timeline use a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 60 | separate keyed FIFO scheduler. Jobs with the same room, target, or direct-chat |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 61 | key run in submission order while unrelated keys run concurrently. Notification |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 62 | commands and encryption commands each have one dedicated serial worker. The |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 63 | encryption worker owns the verification controller and cancels it before |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 64 | shutdown completes. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 65 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 66 | Pagination has a per-timeline mutex gate and a session-owned completion path. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 67 | Pagination tasks are reaped as they finish. Closing a timeline aborts its |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 68 | pagination work and emits one terminal result for every still-pending command. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 69 | There is no public pagination handle. The internal result channel is only the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 70 | session completion mechanism and is not an engine-wide routing design. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 71 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 72 | `TimelineDiff::try_apply` rejects indices that do not match the consumer's |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 73 | snapshot without modifying that snapshot. A consumer then stops applying diffs |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 74 | for that timeline and sends `TimelineCommand::Resync`. The timeline session |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 75 | aborts and joins its old watcher, atomically subscribes to a fresh SDK snapshot |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 76 | and diff stream, and emits `TimelineReset` before the resync command succeeds. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 77 | This works for rooms and threads without closing their timelines or pagination. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 78 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 79 | Timeline diffs and resets carry `TimelineId`, which distinguishes a room from a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 80 | thread by its root event. Thread actors and pagination state belong to the room |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 81 | actor. Stateful timeline commands require the room actor. Standalone room |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 82 | commands such as typing, receipts, send-queue control, membership actions, and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 83 | moderation use the SDK `Room` directly. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 84 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 85 | ## Account construction and secrets |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 86 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 87 | `Session::login_password`, `Session::begin_oauth`, and `Session::restore` use |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 88 | one shared account client builder. It configures encrypted SQLite storage, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 89 | encryption settings, refresh-token handling, and enabled threading without |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 90 | thread subscriptions. `Session::build` passes the authenticated client to the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 91 | engine. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 92 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 93 | Secret-bearing input types do not derive `Clone`. The store passphrase is |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 94 | borrowed by the shared builder, password login passes its password by reference, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 95 | and authentication records are cloned only where SDK ownership requires it. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 96 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 97 | The caller owns durable authentication records. Save `Session::record()` after |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 98 | login, then consume `SessionTokensChanged` and atomically replace that saved |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 99 | record with `ClientHandle::session_record()`. A previously captured record may |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 100 | hold an exhausted refresh token; failing to persist a rotation must be surfaced |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 101 | rather than silently treating the stale record as safe to restore. Restore with |
fa9114fcf75f | atagen | 2026-09-24 12:51 UTC | 102 | the same `StoreConfig` path and passphrase. logout and a rejected token both |
fa9114fcf75f | atagen | 2026-09-24 12:51 UTC | 103 | stop the engine. logout and a hard rejection kill the record and the store, a |
fa9114fcf75f | atagen | 2026-09-24 12:51 UTC | 104 | soft logout keeps the store for a same-device login, `Stopped` alone kills |
fa9114fcf75f | atagen | 2026-09-24 12:51 UTC | 105 | nothing. Neither cookies nor application-specific secret storage belong in the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 106 | toolkit. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 107 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 108 | ## Notifications and room data |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 109 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 110 | The notification client is constructed lazily in an `OnceCell` and shares the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 111 | engine sync service. It is created only when notification resolution is first |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 112 | requested. `PushNotification` exposes typed room and sender metadata and a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 113 | typed `PushNotificationEvent`. The event is `Raw<AnySyncTimelineEvent>` for a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 114 | timeline notification or `Raw<StrippedRoomMemberEvent>` for an invite. SDK UI |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 115 | types do not cross the public boundary. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 116 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 117 | Room summaries are normalised once per `rooms` query. `ClientHandle::rooms_filtered` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 118 | normalises its name search term once before filtering. Summary generation runs |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 119 | display-name requests with bounded concurrency. Sorting is descending latest |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 120 | event timestamp, then case-insensitive name, then room id as a deterministic tie |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 121 | break. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 122 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 123 | Global and room account data retain typed Matrix event-type parameters and raw |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 124 | event content. The handle offers direct raw reads and engine-backed raw writes. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 125 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 126 | ## Commands and errors |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 127 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 128 | `Command` is grouped into `Account`, `Room`, `Timeline`, `Media`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 129 | `Encryption`, `Notification`, and `Lifecycle` domains. `CommandKind` identifies |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 130 | the outer domain in result events. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 131 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 132 | `RoomCommand::InviteUser` contains only room and user ids. It deliberately does |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 133 | not expose a reason because the SDK invite helper accepts none. Kick, ban, and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 134 | unban retain optional reasons where the protocol supports them. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 135 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 136 | `TimelineCommand::Edit` accepts `MessageEditContent`. Its `Text` and `Emote` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 137 | variants carry `RichText`, so edited text and emotes preserve plain text, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 138 | optional Matrix HTML, and mentions. Empty edited bodies fail with `EmptyBody`. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 139 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 140 | `VerificationAction` contains `Confirm`, `Reject`, and `Cancel`. Starting a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 141 | verification is a target-bearing encryption command rather than an action. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 142 | Verification progress uses `Waiting`, `Comparison`, `Confirmed`, `Done`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 143 | `Cancelled`, and typed `Failed(VerificationFailure)`. Request acceptance, SAS |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 144 | startup and acceptance failures retain their message, while stream termination |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 145 | has dedicated variants. A cancelled verification can be started again. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 146 | The watcher and action paths claim terminal emission so a racing cancellation |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 147 | cannot replace `Done` with a local `Cancelled` event. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 148 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 149 | `CommandErrorKind` is the stable typed failure category. It includes room and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 150 | timeline availability, queue pressure, validation, media, verification, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 151 | authentication, network, server, store, timeline, encryption, and fallback |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 152 | categories. The rendered message preserves operation context while internal |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 153 | errors retain their source chains. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 154 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 155 | ## Power levels |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 156 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 157 | Power-level reads return the complete local `RoomPowerLevels` mapping. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 158 | `PowerLevelChanges` applies selected common and well-known event thresholds, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 159 | and `SetUserPowerLevel` changes one user mapping. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 160 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 161 | The SDK trait that applies a power-level event is crate-private. macltok keeps |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 162 | an atomic local mapping, applies all requested changes to it, and sends one |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 163 | state event. It does not patch upstream and does not send separate events for |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 164 | individual fields. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 165 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 166 | ## Public model boundaries |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 167 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 168 | The public model uses owned ruma identifiers and typed data wherever it has a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 169 | stable representation. Raw Matrix event payloads remain `Raw` where arbitrary |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 170 | event content must be preserved. Public output models are non-exhaustive where |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 171 | they are expected to grow. SDK clients remain available through the handle for |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 172 | direct state queries that do not need engine coordination. |