The 33 descriptions (~16k chars, ~19k with paths) overran Codex's skill-list budget (2% of the context window, or 8,000 chars when unknown), and a review found most bodies stale: paths from before the commons/commonsUI moves, the deleted legacy desktop app, a callback signer API that no longer exists, and example APIs that were never in the tree. Kept, rewritten and checked against the code (every path, class and command): account-state (+feeds), amy-expert, android-expert, auth-signers, compose-expert (+API style, Navigation 3 back stacks), desktop-expert (+run/package), event-store-semantics (+NIP-50 search), find-missing- translations, kotlin-coroutines (+structured concurrency, flow modeling), kotlin-multiplatform (+Gradle, composable extraction), ngit-pr, nip85-trusted-assertions, nostr-expert (+NIP lookup, generated NIP catalog), relay-client. Dropped: textbook Compose/Kotlin technique skills, kotlin-expert, kotlin-types-value-class, gradle-expert, quartz-integration (external-facing; see the Quartz docs commit), find-non-lambda-logs (audit finished; the rule is now in AGENTS.md Kotlin Style), and the /nip, /desktop-run, /extract commands. The skill list is now ~4k chars. Also drops .claude/core-skills-plan.md (stale history) and repoints SearchableEvent's KDoc to event-store-semantics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.2 KiB
name: account-state description: Account, LocalCache, User/Note and feeds in commons/model + commons/feeds: per-user XState objects (.flow), FeedFilter/AdditiveFeedFilter, FeedViewModel. Use when adding an account setting or list, a feed, or choosing LocalCache vs account flows.
Account, LocalCache and Feeds
The client state backbone, all headless in commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/
(abbreviated commons/…/ below):
model/cache/LocalCache.kt—object LocalCache : EventCache(). EveryNote/Userthe client has seen. All logic is inmodel/cache/EventCache.kt(an open class so tests can build their own); Android and Desktop both use theLocalCachesingleton (AppModules.kt,DesktopAppModules.kt).model/Account.kt— oneAccountper signed-in user: ~50 per-feature state objects (kind3FollowList,nip65RelayList,muteList,bookmarkState, …), each exposing.flow.feeds/+viewmodels/+<feature>/dal/— feeds: aFeedFilterscansLocalCache, aFeedContentState/FeedViewModelkeeps the result live.
relay EVENT ─► EventCache.justConsume ─► Note/User updated, indexes, live.newNote(...)
│ │
▼ ▼
XState pins its addressable note, LocalCacheFlow.newEventBundles
note.flow().metadata → XState.flow → FeedViewModel → FeedFilter.applyFilter
│ │
└────────► ViewModels / composables ◄┘
Account state objects
Pattern, copied from model/nip51Lists/muteList/MuteListState.kt:
- Pin the note.
val muteListNote = cache.getOrCreateAddressableNote(MuteListEvent.createAddress(signer.pubKey)).LocalCache'snotes/addressablesareLargeSoftCache(weak references), so the state object's field is what keeps its note from being collected. - Expose one
StateFlow. MapmuteListNote.flow().metadata.stateFlow→ decrypt (private lists go through a per-feature…DecryptionCachesuch asMuteListDecryptionCache, held next to it inAccountasmuteListDecryptionCache) → fall back to theAccountSettingsbackup (settings.backupMuteList) when the event is absent →stateIn(scope, SharingStarted.Eagerly, …). - Mutation helpers return a signed event — publishing it is the caller's job.
MuteListState.hideUser(pubkey)returns aMuteListEvent;Account.hideUserdoessendMyPublicAndPrivateOutbox(muteList.hideUser(pubkeyHex)). A state change you never send is invisible to every other client and is lost when the next relay copy arrives. - Consumers read
account.<feature>.flow, never aMutableStateFlowonAccount.
Full catalog of properties, classes, kinds and packages: references/account-state-flow.md.
Adding an account-scoped setting or list
- Persisted as a Nostr event? Pick the kind (NIP-51 list, NIP-78 app data, a relay list) and make
sure quartz has the event class (see
nostr-expert). - Add
commons/…/model/nipXX…/XState.ktmodeled onMuteListState(encrypted) ornip51Lists/BookmarkListState.kt(plain): pin, flow, suspend mutation helpers that build the event with the quartz class and sign it. - Instantiate it as a
valinAccount.kt(plus itsDecryptionCacheif private) and add anAccountmethod that sends the returned event (sendMyPublicAndPrivateOutbox,sendAutomatic, …). If the list must survive relay loss, add abackupXfield toAccountSettings. - Make sure
EventCache.justConsumeInnerInnerlists the event class — a kind that is not listed falls into theelsebranch and is rejected, so the state object never sees it, even thoughEventFactoryparses it. Replaceable kinds without custom logic go in theconsumeBaseReplaceablegroup. - Relay subscription:
relay-client. UI: settings screens are shared incommonsUI/…/commons/ui/screen/loggedIn/settings/; strings follow the AGENTS.md Strings rules.
LocalCache vs Account flow vs feed
| You render… | Read |
|---|---|
| one note / user you have an id for | LocalCache.getOrCreateNote(id) / getOrCreateUser(pubkey), then note.flow().metadata (or replies, reactions, zaps, …) |
| "my follows / mutes / relays / bookmarks" | account.kind3FollowList.flow, account.muteList.flow, account.nip65RelayList.flow, account.hiddenUsers.flow, … |
| a list of notes | a FeedFilter + FeedContentState / FeedViewModel — references/feeds.md |
| ad-hoc filter results | LocalCache.observeNotes(filter: Filter): Flow<List<Note>> |
Never scan LocalCache.notes inside a composable.
Feeds in one paragraph
Bases are in commons/…/feeds/: IFeedFilter / FeedFilter<T> (feed(), feedKey(), limit()
default 500, showHiddenKey()), AdditiveFeedFilter<T> (adds applyFilter + sort for
incremental merges), AdditiveComplexFeedFilter<T, U> (incoming type differs from row type, e.g.
home/dal/HomeLiveFilter.kt), ChangesFlowFilter<T>, FilterByListParams, DefaultFeedOrder /
sortedByDefaultFeedOrder(), FeedContentState, FeedState. Concrete filters are shared, in
commons/…/<feature>/dal/ (home/dal/, hashtag/dal/, notifications/dal/, ~55 of them). The
account's main-tab feeds are long-lived FeedContentStates in viewmodels/AccountFeedContentStates.kt
(owned by BaseAccountViewModel.feedStates); per-screen feeds use a FeedViewModel subclass. Recipe,
real code and traps: references/feeds.md.
Gotchas
LocalCacheis shared across accounts. Switching accounts doesn't wipe it; everyAccountderives its flows from the same cache. Filters that depend on the account must put the account pubkey infeedKey().- Hold the
Note, not theEvent.getOrCreate*returns the same wrapper per id; for replaceable/addressable notesnote.eventis swapped in place when a newer version arrives. - Flows on a
Noteare not durable.CachePruner(model/cache/CachePruner.kt) callsclearFlow()on notes during cleanup, and anything not strongly held can be collected. Re-fetch vianote.flow(); don't stash flow references. - Observers can fire before derived state exists.
EventCachenotifies observers (refreshNewNoteObservers) as part of consuming an event, before some of what it derives from it (channels, indexes) is filled in. Re-read state in the collector rather than trusting it is complete. - Memory pressure trims.
MemoryTrimmingService(model/cache/) is run by both app modules, andAccountFeedContentStatestrims every feed to 200 items onmemoryPressure. Never assume a previously seen note is still resident.
References
references/account-state-flow.md— catalog ofAccountstate objects, kinds and packages.references/local-cache.md—EventCachestructures, insertion path, lookups, pruning.references/feeds.md— adding a feed: filter, state holder, invalidation, rendering, tests.- Related skills:
nostr-expert(event classes,EventFactory),relay-client(subscriptions),auth-signers(how mutations sign),compose-expert(rendering).