atagen

macltok

public
Star0
Watch0

Commit e521a7177da3

tarball

atagen <boss@atagen.co>2026-10-25 13:25 UTC

1 file changed+73-43
docs: record the search review fixes and the log cap decision
Modified file: docs/design-decisions.md+73 additions−43 deletions
@@ -470,9 +470,11 @@ joined room, and answers newest first. it opens a search the way a jump opens an
470470 event context, a session owning the media of every hit it hands out under its
471471 own bounded scope, paged with `search_more` and its cursor and freed with
472472 `close_search`. at most eight stay open and opening another closes the oldest,
473-so a host that never closes anything still holds a bounded amount. a hit's
474-media ids stop resolving once its search closes. encrypted rooms can't use
475-the server's `/search`, so everything goes through one local index, unencrypted
473+so a host that never closes anything still holds a bounded amount, and the
474+closed one goes out as `SearchClosed`. a hit's media ids carry their search's
475+owner prefix like a context's, and stop resolving once it closes. encrypted
476+rooms can't use the server's `/search`, so everything goes through one local
477+index, unencrypted
476478 rooms too, so results and coverage mean the same thing everywhere. a hit comes
477479 back the way event context shows its target, latest edit applied, with byte
478480 ranges of the matched words in its text
@@ -486,60 +488,88 @@ immutable segment with `SingleSegmentIndexWriter` into a `RamDirectory`, edits
486488 and redactions are tombstones in a bitset beside it, and segments of a size
487489 class merge through `merge_filtered_segments` once six pile up, dropping the
488490 tombstoned docs as they go. all of that is single threaded with the docstore's
489-dedicated compressor off. body text is indexed, not stored, a preview reads the
490-event back from the event cache or the server. a 2026-10-25 spike ran the
491+dedicated compressor off. message text is indexed and not stored, a preview
492+reads the event back. the one exception is an edit waiting for its original,
493+whose text is kept so the original can take it over. a 2026-10-25 spike ran the
491494 whole cycle in a headless firefox worker
492495
493-it lives with the store, per login and never synced. natively it's
494-`macltok-search.sqlite3` beside the sdk's databases, in a browser the
495-`::macltok-search` IndexedDB database, and both go when the store is deleted.
496-every segment, every tombstone generation and the manifest is a blob sealed by
497-a `StoreCipher` under the store passphrase, its own key sealed inside so blobs
498-can't be swapped. one commit is one transaction, so a crash keeps the last
499-commit whole. the working copy lives in memory, all of it, which is the price
500-of tantivy's directory being synchronous and IndexedDB not. a store that won't
501-open or read means an index in memory for this engine, and coverage says
502-`persistent: false`
496+it lives with the store, so it's per login and never synced, and it's only
497+opened for the user and homeserver that made it. natively it's
498+`macltok-search.sqlite3` beside the sdk's databases, created 0600, and in a
499+browser the `::macltok-search` IndexedDB database, and both go when the store is
500+deleted. every segment, every tombstone generation, the manifest and the waiting
501+undecryptable events are blobs sealed by a `StoreCipher` under the store
502+passphrase, stored under a keyed hash of their name like the mentions rows, the
503+name sealed inside so blobs can't be swapped. the manifest a commit saves is the
504+index as it stands once that commit lands, and blobs it doesn't point at are
505+dropped on the next load. one commit is one transaction, sqlite in wal with
506+normal sync and IndexedDB relaxed, so a crash can lose the last few seconds of
507+commits but never leaves half of one. the working copy lives in memory, all of
508+it, which is the price of tantivy's directory being synchronous and IndexedDB
509+not. a store that won't open or read, or five commits failing in a row, means
510+an index in memory for the rest of the engine, and coverage says
511+`persistent: false` with the reason in `in_memory`
503512
504513 it's fed from what the client sees. each joined room's event cache is followed
505514 from its first update, so sync, pagination and the redecryptor's replacements
506515 all arrive as diffs, and a room idle for five minutes is let go so the cache can
507-shrink. an edit lands on its original under the original's key, the latest by
508-the original's sender winning, and one that arrives first holds the key
509-provisionally until the original turns up. a redaction drops the key and is
510-remembered for a while, so a stale copy can't bring it back. redacting the
511-latest edit reads the original again. a message still encrypted waits, raw, up
512-to two thousand of them, and is decrypted when a room key for its session
513-arrives
516+shrink. the generic update channel is followed from before the index loads, and
517+if it lags every joined room is followed, since a lost update could be a room's
518+only one. an edit lands on its original under the original's key, the latest by
519+the original's sender winning. one that arrives first waits under its target and
520+its own sender, out of results, until the original turns up and takes the
521+author's edit, so a stranger's early edit can't crowd it out. an edit of an edit
522+is ignored, and an edit to content we can't read clears the old text. a
523+redaction drops the key and is remembered for a while, so a stale copy can't
524+bring it back. redacting the latest edit reads the original again with whatever
525+edits of it are still around. a message still encrypted waits, raw, up to two
526+thousand of them in sixteen blobs, and is decrypted eight at a time when a room
527+key for its session arrives
514528
515529 backfill is `/messages` straight from the room, not through the event cache,
516-so it never grows a timeline someone has open. one request at a time across
517-every room, two seconds apart, a hundred events a page and ten pages a room per
530+so it never grows a timeline someone has open, filtered to messages, encrypted
531+events, redactions, stickers and polls. one request at a time across every
532+room, two seconds apart, a hundred events a page and ten pages a room per
518533 engine run, round robin. a room walks back from the newest event to the start,
519-and resumes from its saved token next time. the first time a room shows up in
520-a run it also gets a head pass from the newest event back until it meets
521-something indexed, which picks up what happened while we were offline. failures
522-back off from a minute to fifteen. all of that is `EngineConfig`, and zero pages
523-a room turns backfill off
534+and resumes from its saved token next time. a room that's been indexed before
535+also gets a head pass the first time it shows up in a run, from the newest event
536+back until it meets something that was already indexed when the engine started,
537+which picks up what happened while we were offline. a failing room backs off on
538+its own from a minute to fifteen. a saved token the server turns down with a 400
539+is dropped and the room starts again from the newest. leaving a room cancels its
540+page. all of that is `EngineConfig`, and zero pages a room turns backfill off
524541
525542 coverage is per room, the oldest and newest indexed message, whether backfill
526-reached the start, and where backfill is at. it comes with every page
527-
528-the analyser splits on anything that isn't alphanumeric, lowercases and folds
529-accents, so `cafe` finds `Café`. it doesn't stem or segment cjk, a run of cjk is
530-one token. words all have to match, quotes make a phrase and `-` excludes.
531-tantivy's merger times itself with `Instant::now()` when a `log` logger is on at
532-debug, which panics in wasm. so in wasm macltok caps `log` at info at compile
533-time, `max_level_info` and `release_max_level_info` both, since in release the
534-release cap wins. a host asking for any other cap gets log's own "multiple
535-max_level_* features set" error rather than a quiet panic later
543+reached the start, and where backfill is at, plus the undecryptable messages in
544+scope. it comes with every page
545+
546+the analyser makes a token of every run of letters and digits up to 64 chars,
547+so hashes and long words fit, and lowercases and folds accents, so `cafe` finds
548+`Café`. han, kana, hangul and thai have no spaces to split on, so every char is
549+a token and so is every pair of neighbours, and a query for a run needs all its
550+pairs. that finds a word inside a sentence at the cost of the odd match across a
551+word boundary. emoji are tokens too. queries have no field syntax, so `re:` or a
552+url is just its words. words all have to match, quotes make a phrase and `-`
553+excludes, and a query with nothing left to find is `Invalid` rather than
554+everything in scope. there's no stemming
536555
537556 a hit is read back when it's shown, `load_or_fetch_event` for the original and
538557 for its latest edit, which is the event cache and its store first and the
539-server's `/event` after. one that's turned out redacted is dropped from the page
540-and from the index. one that can't be read at all, gone from the cache with the
541-server unreachable or refusing, is left out of that page but stays indexed, so
542-it shows up again once it can be read. the cursor still moves past it
558+server's `/event` after, eight at a time. the original's bundled edit is ignored,
559+since the index already knows the newest one. one that's turned out redacted is
560+dropped from the page and from the index. one that can't be read at all, gone
561+from the cache with the server unreachable or refusing, is left out of that page
562+but stays indexed, so it shows up again once it can be read. the cursor still
563+moves past it
564+
565+tantivy's merger times itself with `Instant::now()` whenever a `log` logger is on
566+at debug, and that panics in wasm. on 2026-10-25 we capped `log` at info at
567+compile time in wasm, `max_level_info` and `release_max_level_info` both, since
568+in release the release cap wins. it's all or nothing for the whole wasm build, so
569+a host gives up `log` debug and trace from every crate, not just tantivy, and
570+asking for any other cap is log's own "multiple max_level_* features set" error.
571+tantivy timing with something wasm can call, or reading `Instant` only behind
572+its own feature, reopens it
543573
544574 ## the sdk fork