Skip to content

feat: keep every room's history on the device and put it back on relays that forgot - #199

Merged
TheCryptoDonkey merged 6 commits into
mainfrom
feat/room-archive
Sep 28, 2026
Merged

TheCryptoDonkey merged 6 commits into
mainfrom
feat/room-archive

Conversation

@TheCryptoDonkey

Copy link
Copy Markdown
Member

Part 1 (steps 1-3) of the 27 Sept durable-history plan, web and desktop. The owner's two-person room showed 425 messages on the desktop and none on Android because only one relay still held the chat and no client kept a copy.

  • app/src/room-archive.ts: IndexedDB archive of the ORIGINAL signed events (chat on every channel incl. control, plus the authority's rekeys), sealed with AES-GCM under a non-extractable device key; record names are keyed HMACs, AAD binds each record to its conversation. Header comment is honest about limits (keys live in the same IndexedDB on disk; sizes and per-conversation counts show).
  • Read path (src/chat.ts, src/session.ts): archive first, then relays, through the same decodeChatEvent/rekey path and rules. loadOlder() pages past the 30-day/500 window; decode in chunks of 40 with yields; memory released when a log closes.
  • Reseed: per relay, paged with until, only complete answers count (query returns { events, complete }), nothing older than the oldest returned counts as missing; republished unchanged via publishQuietly (no health marks, no reconnects, not counted as publishing); bounded (500/conversation, 1,000/pass, 100 ms apart, 10 min per relay+conversation); never quiet-room events (marked inside the sealed record).
  • Cap evicts oldest; kept only after retention/dedupe/rate checks; lane chip filled from the first relay copy.
  • "Forget this browser" deletes the archive.

Independent review found no security hole; all seven findings fixed (bc05e29). The relay "does not keep chat" flag lives in #198 instead.

Tests: new unit suites (archive, room-archive, relay-pool, chat); typecheck; test/room-archive.spec.ts (relay forgets, device still shows history, puts it back, newcomer reads it) chromium+firefox; related specs 29/29 chromium.

Merge after #196 and #198 (all touch src/relay-pool.ts). Not in scope: Android archive, catch-up from peers/Bothy, retention rule (Part 3).

🤖 Generated with Claude Code

https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk

A chat log given an EventArchive shows what the device kept before any
relay answers, keeps every event it accepts (and its own sends), and pages
back past the 30-day/500 render window with loadOlder. Archived events go
through decodeChatEvent exactly as relay ones do. A session keeps the
authority's rekeys and replays them at join, so a forgotten relay cannot
leave a device in an old epoch. reseedCandidates decides what a relay that
returned fewer events should be handed back.

Claude-Session: https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk
…gets chat

NostrRelayPool gains query and publishTo for a single configured relay and
noteKeepsChat; relay settings show 'Does not keep chat' for a relay that
accepted room chat and returned none of it.

Claude-Session: https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk
…ful relays

Original signed events are sealed per device in IndexedDB under a
non-extractable AES-GCM key; records carry only HMAC handles, so the disk
shows no room ids, event ids or times. Rooms open on archived history,
page back when the reader reaches the top, and five seconds after joining
hand each room relay that returned fewer events the originals it lacks,
rate-limited and bounded, never a quiet room's chat. Forgetting this
browser deletes the archive.

Claude-Session: https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk
Reseed compares a relay page by page with `until`, so a relay that caps
its answers is read to the end, and judges nothing older than the oldest
event it returned when paging stops early. `query` now says whether the
relay really finished; a slow or closed read is unknown, and nothing is
republished on it. Reseed writes through a new quiet `publishQuietly`:
one attempt, no health marks, no reconnect, not counted as publishing.
The archive-side "does not keep chat" flag and its relay settings line
are gone; relay health detects that elsewhere.

The archive drops the oldest events past its per-conversation cap
instead of refusing new ones, and a chat log keeps an event only after
the retention, duplicate and per-sender rate checks. A message first
read from the archive takes its lane from the first relay copy that
arrives. Archived history decodes in chunks with the page given a turn
between them; a read waits only for its own conversation's pending
writes; a closed or rekeyed log lets its conversation go from memory.
Events said in a quiet room are marked inside the sealed record and
never handed back to a relay. The header now says plainly what the
device keys do and do not protect, and that records are not padded.

Claude-Session: https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk
@TheCryptoDonkey
TheCryptoDonkey merged commit f346882 into main Sep 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant