# macltok design decisions

This document describes the contract implemented in the working tree.

## Engine execution

The engine keeps a contextual task registry. Every spawned task is registered
with either its command id and kind, a watcher name, or a room id and
generation. The run loop continuously joins completed tasks and removes their
context. A command-task panic produces its correlated `CommandFailed` event.
A watcher or current room-actor panic is terminal to the engine. Shutdown aborts
and joins the remaining registered tasks.

The engine emits `Authenticated`, then `ConnectionChanged(Connecting)`, before
starting the sync service. `ConnectionChanged` also represents the later SDK
states and the final `Stopped` transition. `Engine::run` returns an
`EngineError` for terminal failure and emits `Failed` before `Stopped` when the
event stream remains available.

The driver command channel remains lossless and clients must drain
`EventStream` independently of command submission. `ClientHandle` also exposes
a `watch` receiver for the current connection state. `RoomsChanged` is an
invalidation event. Call `ClientHandle::rooms` for current, normalised room
summaries.

`ClientHandle` sends `Command` values and receives a `CommandId`. Completion is
reported as `CommandSucceeded`, `CommandSucceededWithRoom`, or
`CommandFailed`. `CommandFailure` contains the id, outer `CommandKind`, typed
`CommandErrorKind`, and display message. Direct request and response methods
return `CommandResultError`, which is either a delivered `CommandFailure` or a
closed reply channel.

For timeline sends, success means the SDK send queue accepted the event;
network delivery is reported later through timeline send state.

`send` reports enqueueing, not completion. Consumers retain the returned id
until a terminal result with that id arrives; they match `CommandFailure::kind`
rather than parsing its display message. The event stream must be drained while
commands are submitted, even when several frontend clients share one engine.

## Room lifetime and ordering

An open room has an explicit generation and exactly one of three states.

- `Opening` owns the generation and actor request sender
- `Open` owns the generation and actor request sender
- `Closing` owns the generation and a bounded FIFO of deferred open and close commands

An `Open` command starts an actor immediately and returns its terminal result
only after the actor reports readiness. There is no separate readiness waiter or
readiness task. Repeated opens while opening are retained by the actor and each
gets one terminal result. A close changes the room to `Closing`, rejects work
that needs an open actor, and waits for that generation to exit. Opens and
closes during closing are queued in order; after exit, each deferred command is
replayed, with a fresh generation started when needed. Events from earlier
generations are ignored.

Each room actor serialises its timeline work in FIFO order. Different rooms run
concurrently. Room mutations which do not require an open timeline use a
separate keyed FIFO scheduler. Jobs with the same room, target, or direct-chat
key run in submission order while unrelated keys run concurrently. Notification
commands and encryption commands each have one dedicated serial worker. The
encryption worker owns the verification controller and cancels it before
shutdown completes.

Pagination has a per-timeline mutex gate and a session-owned completion path.
Pagination tasks are reaped as they finish. Closing a timeline aborts its
pagination work and emits one terminal result for every still-pending command.
There is no public pagination handle. The internal result channel is only the
session completion mechanism and is not an engine-wide routing design.

`TimelineDiff::try_apply` rejects indices that do not match the consumer's
snapshot without modifying that snapshot. A consumer then stops applying diffs
for that timeline and sends `TimelineCommand::Resync`. The timeline session
aborts and joins its old watcher, atomically subscribes to a fresh SDK snapshot
and diff stream, and emits `TimelineReset` before the resync command succeeds.
This works for rooms and threads without closing their timelines or pagination.

Timeline diffs and resets carry `TimelineId`, which distinguishes a room from a
thread by its root event. Thread actors and pagination state belong to the room
actor. Stateful timeline commands require the room actor. Standalone room
commands such as typing, receipts, send-queue control, membership actions, and
moderation use the SDK `Room` directly.

## Account construction and secrets

`Session::login_password`, `Session::begin_oauth`, and `Session::restore` use
one shared account client builder. It configures encrypted SQLite storage,
encryption settings, refresh-token handling, and enabled threading without
thread subscriptions. `Session::build` passes the authenticated client to the
engine.

Secret-bearing input types do not derive `Clone`. The store passphrase is
borrowed by the shared builder, password login passes its password by reference,
and authentication records are cloned only where SDK ownership requires it.

The caller owns durable authentication records. Save `Session::record()` after
login, then consume `SessionTokensChanged` and atomically replace that saved
record with `ClientHandle::session_record()`. A previously captured record may
hold an exhausted refresh token; failing to persist a rotation must be surfaced
rather than silently treating the stale record as safe to restore. Restore with
the same `StoreConfig` path and passphrase. logout and a rejected token both
stop the engine. logout and a hard rejection kill the record and the store, a
soft logout keeps the store for a same-device login, `Stopped` alone kills
nothing. Neither cookies nor application-specific secret storage belong in the
toolkit.

## Notifications and room data

The notification client is constructed lazily in an `OnceCell` and shares the
engine sync service. It is created only when notification resolution is first
requested. `PushNotification` exposes typed room and sender metadata and a
typed `PushNotificationEvent`. The event is `Raw<AnySyncTimelineEvent>` for a
timeline notification or `Raw<StrippedRoomMemberEvent>` for an invite. SDK UI
types do not cross the public boundary.

Room summaries are normalised once per `rooms` query. `ClientHandle::rooms_filtered`
normalises its name search term once before filtering. Summary generation runs
display-name requests with bounded concurrency. Sorting is descending latest
event timestamp, then case-insensitive name, then room id as a deterministic tie
break.

Global and room account data retain typed Matrix event-type parameters and raw
event content. The handle offers direct raw reads and engine-backed raw writes.

## Commands and errors

`Command` is grouped into `Account`, `Room`, `Timeline`, `Media`,
`Encryption`, `Notification`, and `Lifecycle` domains. `CommandKind` identifies
the outer domain in result events.

`RoomCommand::InviteUser` contains only room and user ids. It deliberately does
not expose a reason because the SDK invite helper accepts none. Kick, ban, and
unban retain optional reasons where the protocol supports them.

`TimelineCommand::Edit` accepts `MessageEditContent`. Its `Text` and `Emote`
variants carry `RichText`, so edited text and emotes preserve plain text,
optional Matrix HTML, and mentions. Empty edited bodies fail with `EmptyBody`.

`VerificationAction` contains `Confirm`, `Reject`, and `Cancel`. Starting a
verification is a target-bearing encryption command rather than an action.
Verification progress uses `Waiting`, `Comparison`, `Confirmed`, `Done`,
`Cancelled`, and typed `Failed(VerificationFailure)`. Request acceptance, SAS
startup and acceptance failures retain their message, while stream termination
has dedicated variants. A cancelled verification can be started again.
The watcher and action paths claim terminal emission so a racing cancellation
cannot replace `Done` with a local `Cancelled` event.

`CommandErrorKind` is the stable typed failure category. It includes room and
timeline availability, queue pressure, validation, media, verification,
authentication, network, server, store, timeline, encryption, and fallback
categories. The rendered message preserves operation context while internal
errors retain their source chains.

## Power levels

Power-level reads return the complete local `RoomPowerLevels` mapping.
`PowerLevelChanges` applies selected common and well-known event thresholds,
and `SetUserPowerLevel` changes one user mapping.

The SDK trait that applies a power-level event is crate-private. macltok keeps
an atomic local mapping, applies all requested changes to it, and sends one
state event. It does not patch upstream and does not send separate events for
individual fields.

## Public model boundaries

The public model uses owned ruma identifiers and typed data wherever it has a
stable representation. Raw Matrix event payloads remain `Raw` where arbitrary
event content must be preserved. Public output models are non-exhaustive where
they are expected to grow. SDK clients remain available through the handle for
direct state queries that do not need engine coordination.
