This commit is contained in:
2026-09-22 16:03:03 +07:00
parent 28f4c1596d
commit 6fac58d494
7 changed files with 774 additions and 117 deletions
+23 -5
View File
@@ -274,7 +274,10 @@ than writing off a relay that only wanted to authenticate.
reports what it saw: `oldest_ms`/`newest_ms` feed the caller's `ChannelCursor`,
`exhausted` is earned only by a short page *after* history was seen, and an
all-empty answer sets `failed` so a later round re-asks instead of sealing the
channel at "no more history". Page down with `Window::older_than(seen.oldest_ms)`,
channel at "no more history". `unreadable` counts the wraps the page reached that
no held key could open — sealed past the cutoff their key's rotation set, or bound
to another channel — because those are history the reader is missing, not history
that is not there. Page down with `Window::older_than(seen.oldest_ms)`,
open a channel with `Window::opening(cursor)` (wide cold, `newest_ms - 60s` warm),
and read the region between two cursors with `Window::between(..)`. `query_rumors`
is the read path when the group keys are gone; pass `kinds` to budget rows apart
@@ -284,6 +287,18 @@ author and message times without re-opening a wrap it already cached, and
`cache::purge_expired(client, &channel, now)` runs at the top of every round — the
timer is cooperative, so the local store is the artifact that has to forget.
`sync::fold` counts the same thing over the whole store, per channel, in
`Snapshot.unreadable`. `sync_round` sums a round's pages into
`Progress.unreadable`, and `Community` keeps both per channel: `progress(channel)`
is the last completed round, `unreadable(channel)` is the count, and
`missing_key(channel)`, `channel_removed_at(channel)`, `removed_at()` and
`stranded()` are the rest of the honest read surface a panel needs to tell an
empty room from one it cannot read. `Community::due(channel)` is the other half —
whether a round is worth asking for yet (a round for `Older` and an explicit retry
never ask it), and `Community::tick`, driven by `CommunityRegistry` once per
`community::MIN_ROUND_INTERVAL`, re-folds and re-rounds the active channel of a
community whose last round is older than `STALE_AFTER`.
`ChatAction::TimerNotice { seconds }` is a policy notice, not a message: render it
as an inline row only when its author passes
`control.roles.is_authorized(&author, &owner, Permissions::MANAGE_METADATA)`.
@@ -717,7 +732,10 @@ client.subscribe(filter).with_id(sub_id).await?;
retired root's Control signer and the publish time that retires it), and each
`ChannelKeyRef.priors` keeps every channel key a rotation stepped off. The read
side derives planes from all of them, and a retired key's `retired_at` is a hard
read cutoff at both `history::page` and `sync::fold`. The residual gaps: the
panel does not yet render the `removed`/`stranded` state `Community::removed_at`
and `Community::stranded` carry (phase 4), and a base removal is not yet enforced
at send time — the composer still has the old root to write under.
read cutoff at both `history::page` and `sync::fold`. The state
`Community::removed_at`/`Community::stranded`/`channel_removed_at` carries is
rendered as a notice **and** enforced at write time: `Community::send` refuses
when `channel_secret` is `None` (a removal, a strand, a channel cut, or a key we
never held), because a wrap sealed under a superseded root would reach nobody who
rotated. The residual gap: this client does not adopt a rotation it published
itself, because there is no rekey writer yet.