Regression: after wiring CORD-06, joined communities showed but their
channels were empty. Folding the Guestbook + next-epoch base-rekey planes
into the SAME kind-1059 REQ and NIP-42 stream-key AUTH set as the control
and channel planes starved the whole subscription on relays that gate a
REQ on stream-key AUTH: the control plane stopped folding, so no channels
appeared (the community list is a separate kind-13302 fetch, so it still
showed).
Restore the control + channel subscription and AUTH set to exactly their
pre-CORD-06 form: drop auxiliaryPlaneSubs from the shared REQ, and drop the
Guestbook/next-rekey keys from streamKeys() (moved to auxStreamKeys() for a
future isolated subscription). The receive-side pieces (guestbook fold,
rekey buffering/drain) stay in place but are dormant until re-introduced in
their own subscription that cannot affect the core chat path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Ban is only a soft removal — the member still holds the room key and can
decrypt everything; clients just decline to show their posts. This wires
CORD-06 Refounding: the hard removal that rotates the community_root so a
removed member's key stops working for anything sent afterwards.
Quartz:
- ConcordKeyDerivation: baseRekeyAddress / channelRekeyAddress (the rekey
stream addresses) and epochKeyCommitment (prevcommit, CORD-02 A.5).
- ConcordRekey: signer-based blobForSigner / findNewKeyWithSigner so a
bunker account opens its blob with a single nip44Decrypt.
- ConcordRefounding: compactControlPlane (re-wrap each head edition's
original plaintext seal under the new root, preserving signatures),
buildBaseRekeyWraps, build, findNewRoot. OpenedStreamEvent now carries
the inner seal for compaction. ConcordRefoundingTest.
Commons:
- ConcordActions: guestbookPlane / nextBaseRekeyPlane, buildGuestbookJoin /
guestbookMembers, buildRefounding, openBaseRekey.
- ConcordCommunitySession folds the Guestbook plane into members (the
recipient set), buffers inbound base-rekey wraps, exposes controlPlaneWraps,
and AUTHs to + subscribes the Guestbook and next-epoch base-rekey planes.
- ConcordSessionRegistry.sync rebuilds a session when its entry's root/epoch
changed; ConcordSubscriptionPlanner.auxiliaryPlaneSubs REQs the new planes.
Amethyst:
- Account announces a Guestbook JOIN on create/join; refoundConcordCommunity
(owner / BAN-holder) bans + rolls + publishes + persists; drainConcordRekeys
adopts an inbound rotation from an authorized rotator; adoptConcordRoot
persists the new root (prior kept as a HeldRoot) and re-seeds the new epoch's
Guestbook, guarded against double-adopt.
- AccountViewModel.removeConcordMember; ConcordMembersScreen "Remove from
community" action + confirmation dialog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
The composer's Notify row previously showed only the parent's p tags and
its author, each with an x that removed the user for good. It now lists
every thread member (authors along the reply chain plus the parent's
mentions), and each chip carries a bell instead: tapping it mutes the
notification for that user but keeps the chip — faded and with a
bell-off icon so the state is obvious — making it one tap to add them
back. Muted members are dropped from the outgoing event's p tags (and
from a private note's receivers), and drafts round-trip the muted state
by re-deriving thread members whose p tag the draft dropped.
The NIP-22 comment composer gets the same chip semantics; there the
mute is honored for the optional zap-sender tag, while the structural
root/reply scope tags stay as NIP-22 requires.
Adds the notifications_off glyph to the Material Symbols subset font.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013eK8mGPcuKYXNyhKVJ8Wg7
Adds the two-way reply model: an INLINE reply stays in the timeline (native chat
message referencing its parent), a MINICHAT reply is a kind-1111 NIP-22 comment
pulled into a thread opened from the parent — matching Armada.
Foundation (quartz/commons):
- ReplyMode enum (INLINE default, MINICHAT).
- ChannelChat.inlineReply / ConcordActions.buildChannelInlineReply restore the
kind-9 q-tag quote path alongside the existing kind-1111 ChannelChat.reply.
- ChannelFeedFilter excludes kind-1111 CommentEvents from the chat timeline (they
belong in the minichat), so inline replies stay and thread replies move aside.
- observeNoteMinichatReplyCount: local count of a message's kind-1111 replies.
Concord composer + send (amethyst):
- ConcordNewMessageViewModel gains a replyMode state + toggle; the composer shows a
"In chat" / "In thread" toggle beside the reply preview.
- Account.sendConcordChannelMessage routes MINICHAT to kind-1111, INLINE to kind-9
quote, fresh post to kind-9 message.
Shared row chip (all chat types):
- ChatMessageCompose's action row shows an "N replies" chip when a message has
kind-1111 thread replies; tapping opens the thread. Wired to the thread view for
now; a chat-styled minichat screen and NIP-28/NIP-29 loading follow.
Plan: amethyst/plans/2026-07-12-dual-reply-minichat.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Concord replies were kind-9 chat messages carrying a `q` tag. In the Concord
model (matching Soapbox Armada) a `q` on a kind-9 is an inline *quote*, which
clients deliberately keep OUT of threads — a real thread reply is a kind-1111
NIP-22 comment. So our replies rendered (and were sent to Armada) as inline
quotes, never grouping into a message's thread.
ChannelChat.reply now builds a CommentEvent via CommentEvent.replyBuilder: the
uppercase K/E/P tags pin the immutable thread root and the lowercase k/e/p tags
point at the immediate parent (root inherited when the parent is itself a
comment, so the root is stable at any depth), plus the same channel/epoch binding
every Chat Plane rumor carries. This is byte-compatible with Armada's
buildV2CommentTags, so replies thread correctly in both directions. The read path
already accepts these (they carry the binding, and consumeConcordRumor handles
CommentEvent), so incoming Armada thread replies now land bound to their channel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Reverts the Kover coverage aggregation (introduced in dca345212b) from every
module, the root aggregation block, the Sonar coverage import, the version
catalog, and BUILDING.md. With the plugin present the 16 GB CI runner OOM-killed
during the Android test+build job even with Kover opt-in disabled; removing it
entirely clears the OOM. No other CI changes — the diff versus main is exactly
the inverse of dca345212b.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Concord relays gate a plane's kind-1059 wraps behind NIP-42 and serve them only
to a connection authenticated AS the plane's derived stream key — a Concord wrap
is authored by the stream key and p-tagged to a throwaway ephemeral key, so the
member is neither author nor recipient. Amethyst authenticated only as the user,
so every plane REQ was refused ('auth-required: all authors must be
authenticated') and no channel or message ever loaded, even though the community
list (kind 13302) folded correctly.
Amethyst's relay-auth plumbing already supports multiple identities per
connection: RelayAuthenticator sends one AuthCmd per event in the list its
provider returns, deduped on (pubkey, challenge). This wires the derived stream
keys into that provider:
- ConcordCommunitySession.streamKeys() exposes control + folded-channel GroupKeys.
- ConcordSessionManager.streamAuthSecretsFor(relay) returns the stream secret keys
a NIP-42 challenge from that relay must be answered with (relay-scoped to each
community's own relays — the same scope the plane subscription uses).
- AuthCoordinator signs one kind-22242 per stream key locally (raw KeyPair via
NostrSignerSync — never the account signer, never exposing user identity), and
attaches them to every challenge independent of the user-auth policy. On auth
success the existing syncFilters re-fires the previously-refused plane REQ.
- RelayAuthStatus LruCaches widened 10 -> 200 since one connection now
authenticates as the user plus many plane keys (control + channels).
Mirrors Armada's streamAuth.ts (sign one AUTH per scoped stream key, accumulating
on the connection). Verified live on-device by the debug pass: authing as the
derived stream key returns the control-plane wraps that were previously refused.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Removes the last catch-all ConcordKinds constant object. Each remaining kind
now lives on the class that owns it:
- wrap/seal kinds were already ConcordStreamEnvelope.KIND_WRAP/SEAL_* — callers
(ConcordActions, ConcordSubscriptionPlanner) reference those directly.
- guestbook join/leave (3306) and kick (3309) become Guestbook.KIND_JOIN_LEAVE /
KIND_KICK.
- direct-invite (3313), voice-presence (23313), and rekey (3303) inline their
literals on ConcordDirectInvite/VoicePresence/ConcordRekey.
The unused edit/webxdc/typing/snapshot/invite-list/ephemeral-wrap constants are
dropped; they will own their literals when those events get dedicated classes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Removes the CONTROL/COMMUNITY_LIST/INVITE_BUNDLE constants from ConcordKinds
now that each has a dedicated Event class. Callers reference
ControlEditionEvent.KIND, ConcordCommunityListEvent.KIND, and
ConcordInviteBundleEvent.KIND directly, matching the nip88Polls per-kind
event convention.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
- Apply Kover (Apache-2.0, build-time only) to the KMP/JVM modules and aggregate at the root
- include amethyst playDebug in the aggregated Kover coverage
Each PoW job now races PoWPolicy.minerWorkers(cores) = cores/2 parallel
searches (at least 1) instead of a single thread, so a post on an 8-core
phone mines ~4x faster while the other half of the cores stays free for
the UI, and the total CPU budget matches the old 2-job x 1-thread pool.
- PoWPolicy.minerWorkers(availableProcessors) is the single definition of
the per-job worker budget.
- PoWPublishQueue gains minerThreads (used by its built-in miner and
exposed for custom mine lambdas); AppModules wires maxConcurrent = 1 +
minerThreads = half the cores.
- PoWNostrSigner gains a workers param (default 1), covering reactions,
reposts and reports via Account.miningSigner; the anonymous-post paths
in the short-note and comment composers pass the same budget.
- Gift-wrap envelope mining stays single-threaded: the template
conversion is a non-suspend hook inside the synchronous NIP-59 build.
- UI time estimates (settings picker, composer chip, broadcast banner)
now benchmark at the queue's worker count via deviceHashesPerSecond(),
and PoWEstimator caches one rate per worker count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhaF5scnAvhP9wr9R7bTWM
First slice of the nip88Polls-structure refactor (plan in quartz/plans/
2026-07-11-concord-event-classes.md). A Concord chat rumor IS a standard Nostr
event plus a channel/epoch binding, so stop re-deriving it by kind number:
- New cord03Channels/tags/ChannelTag + EpochTag (typed, parse/assemble), plus
TagArrayBuilderExt (channel/epoch/channelBinding) and TagArrayExt
(concordChannel/concordEpoch/isConcordBoundTo) — the poll-package shape.
- ChannelChat.message/reply now build via ChatEvent.build{ channelBinding(...) }
and assemble the template into a rumor; reaction reuses ReactionEvent.KIND. The
minimal e/p/k reaction tags and q/p reply tags are kept byte-identical for Armada
interop (guarded by the Concord round-trip tests, which pass).
- Drop the ConcordKinds.MESSAGE/COMMENT/REACTION/DELETE aliases (they shadowed
ChatEvent/CommentEvent/ReactionEvent/DeletionEvent KINDs); callers use the real
event KINDs. VoicePresence uses the new tag classes instead of bindingTags.
Remaining Concord-specific kinds (control 3308, invites, guestbook, rekey, voice,
seals/wrap) get their own per-kind packages in later slices per the plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
The miner enumerates the nonce space deterministically (the random base is
overwritten before the first hash), so naively racing N copies of the search
duplicates the exact same candidate sequence N times. PoWMiner.mine() now
races workers over disjoint slices instead: each worker's nonce carries a
distinct fixed prefix while only the bytes after it are enumerated, making the
aggregate hash rate scale with cores (~3.8x on a 4-core box).
The hot loop also switches from sha256() to sha256Into() with a reused
32-byte buffer, so hashing no longer allocates per attempt.
amy wiring:
- `pow mine` and `post --pow` mine on all cores by default; `pow mine
--threads N` overrides.
- `pow bench` measures the all-cores rate (what mining now uses, also the
basis for expected_seconds) alongside a new hashes_per_second_single_core.
- PoWEstimator benchmarks with sha256Into to match the miner, and gains a
workers overload that prices in cross-core contention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhaF5scnAvhP9wr9R7bTWM
Completes the parity gaps with NIP-29 relay groups:
- Rich invite card (ConcordInviteCard): an invite link in note content now renders
as a tappable card that peeks the kind-33301 bundle (Account.peekConcordInvite) to
show the community name, instead of a bare link. Wired into RichTextViewer.
- Bare naddr (kind 33301) in ClickableRoute now shows an informative label rather
than an empty addressable-note card (a naddr has no unlock token, so it can't be
joined — only the full link can).
- External invite URLs open the app: AndroidManifest intent-filter for
amethyst.social/invite/*, and MainActivity.uriToRoute maps the full URL (fragment
included) to Route.ConcordInvite so the redeem flow keeps the token.
- Messages group-by-community view mode (ConcordViewMode INLINE/GROUPED), the
analog of NIP-29's group-by-relay: GROUPED collapses each community's channels
into one ConcordServerRoomNote row (rendered by ConcordServerRoomCompose, opens the
channel list). Adds updateConcordViewMode + LocalPreferences persistence + feed
invalidation + the ChatroomListKnownFeedFilter branch + a Messages-settings toggle.
Also resolves a silent merge artifact: main's markDmRoomAsRead(signedEvents.msg)
landed in the wraps-only broadcastPrivately overload (no signedEvents in scope);
moved it to the Result overload that carries .msg.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Adds the two management screens the NIP-29 relay groups have but Concord lacked,
plus a visual overhaul of the create screen:
- Members roster (ConcordMembersScreen): owner + role-holders + banned, each with
avatar/name and owner/admin/banned badge; overflow menu promotes/demotes (owner)
and bans/unbans, gated on the viewer's authority. New AuthorityResolver.roleHolders()
and bannedMembers() accessors expose the privileged roster (membership is otherwise
key possession, so there is no silent-member list).
- Edit metadata (ConcordEditScreen): new write path — ConcordModeration.editMetadata
(METADATA edition, entityId = communityId) + Account.editConcordMetadata; prefilled
from the folded state, honored on fold only for MANAGE_METADATA holders / owner.
- Create screen redesign: shared ConcordMetadataFields with a circular icon hero
(live preview from the URL) + section headers, mirroring the NIP-29 GroupImagePicker
layout, replacing the bare stack of text fields.
- Routes ConcordMembers/ConcordEdit + nav registration + top-bar entry points on the
channel-list screen (Members always, Edit for those who can manage metadata).
- Account.peekConcordInvite for the upcoming invite card.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
The per-post PoW button on the New Post screen now sits before the
Subject/Title toggle instead of last in the options row.
Every PoW surface (composer button, note chip, mining banner, settings tile,
relay-info minimum-PoW row) switches from the Bolt icon to Manufacturing
(gear, U+E726): Bolt is the app's zap/lightning glyph everywhere else, so a
Bolt PoW badge next to the zap-split/zapraiser/invoice buttons read as a
payment feature. Subset font regenerated for the new codepoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Durability & correctness:
- Keep the job entry + disk checkpoint alive until the publish continuation
completes (was: dropped at mining-complete); failed publishes keep their
checkpoint for restart retry and surface through a failures SharedFlow
- Move draft deletion into the publish continuations in every composer so a
cancelled or process-killed mining job can't destroy the only copy of a post
- Persist gift-wrap mining: split NIP17Factory into createSeals (signer
interaction, runs inline) + wrapSeal (pure CPU, runs on the queue), new
REPLAY_WRAPS records restore pending DM wraps after process death
- Clamp synced NIP-78 difficulty (PoWPolicy.MAX_DIFFICULTY), require a sane
target in PoWMiner, bound PoWRankEvaluator against short ids
- Reaction double-tap while mining now toggles (dedupeKey + cancelByKey)
instead of publishing duplicates
- Restore checkpoints for every loaded account, not just the active one
- logOff purges the account's checkpoints and cancels its queued jobs
- Private notes: composer chip now gates on the gift-wrap kind and the
per-post override reaches sendPrivateNote
Coverage:
- Public/live chat (kinds 42 + 1311) and voice replies (1244 + kind-1 audio
replies) now route through the mining gate
Perf:
- FGS start() dedupes with a running flag; PendingIntents built once
- Banner 1 Hz clock only ticks while a job shows elapsed time
- PoWEstimator benchmark is single-flight behind a Mutex
UX / l10n:
- Post-mining failures toast with retry information
- Count strings converted to <plurals>; elapsed time via DateUtils; settings
estimate uses localized units; shared powKindLabelRes replaces three
duplicated kind→label maps; dead pow_chip_* strings removed
- CLI: pow mine lowercases the pubkey before mining (uppercase hex mined an
id that never matches the signed event) and validates via quartz Hex
New queue tests: checkpoint lifetime, failure reporting, cancelByKey toggle,
per-owner cancellation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
The Android assembler was hand-rolling the plane-address -> per-relay
kind-1059 RelayBasedFilter collapse (with per-relay since). That logic is
platform-agnostic — RelayBasedFilter and SincePerRelayMap are both
commons/quartz types — so lift it to ConcordSubscriptionPlanner.relayBasedFilters
alongside the existing filtersByRelay one-shot variant. The assembler now only
does the account-dependent step (deriving channel planes from folded session
state) and delegates the collapse. Covered by a new planner unit test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
- ConcordHomeScreen: each community row now shows a circular avatar
(RobohashFallbackAsyncImage, community icon or robohash fallback) and a
pluralized channel count, re-read reactively on each Control-Plane fold.
- ConcordChannelListScreen: channel rows show a type icon (# public / lock
private / mic voice) and an explicit empty state, styled like the relay-group
channel list (thin dividers).
- Inbox row (ConcordRoomCompose): passes the folded community icon to ChannelName
instead of null, so Messages shows the community avatar.
- ConcordChannel exposes communityIcon from the folded MetadataEntity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Replaces the comma-separated relay text box with the app's RelayUrlEditField
(type a relay → NIP-11 name/icon autocomplete, tap to add), shown above a
removable list of chosen relays. Adds an optional community icon URL, plumbed
through createConcordCommunity → ConcordActions.createCommunity →
ConcordCommunityFactory into MetadataEntity.icon. Section header styling matches
RelayGroupMetadataScreen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Adds an owner-only role toggle to the Concord note quick-action menu, alongside
Ban:
- Account.concordAdminTarget gates the action to the owner (only rank 0 strictly
outranks the position-1 Admin role, as the resolver requires), never the
owner's own note or the owner as target, and reports whether the author is
already an admin (via the new AuthorityResolver.rolesOf accessor).
- makeConcordAdmin mints a default Admin role (all management + moderation
permissions, position 1) if the community doesn't have one yet, then grants it;
removeConcordAdmin revokes via an empty grant.
- The menu item flips between "Make admin" and "Remove admin" and fires
toggleConcordAdmin.
Extends ConcordModerationTest to cover rolesOf and revoke-via-empty-grant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Restructure the global auth control into a top-level mode — Always authenticate,
Never authenticate, or Custom — where Custom reveals independent per-situation
toggles instead of the confusing single sub-toggle:
- My relays and venues (own relays + joined/subscribed/favorited venues) — on
- Read posts from people I follow — on
- Message people I follow (DMs, replies, notifications) — on
- Message anyone / strangers — off by default (you're asked each time instead)
RelayAuthPolicy is now {ALWAYS, NEVER, CUSTOM}. The resolver takes a
RelayAuthCustomToggles plus split serves-facts (followed-read, followed-write,
stranger-write, own-relay, venue) and, under CUSTOM, allows if any enabled
category matches — else falls through to a prompt. There is deliberately no
"read strangers' posts" category, so that always prompts.
Account settings replace the single delivery flag with four persisted booleans
(default policy CUSTOM; no migration, unreleased). The contextual DM/notification
prompt button now switches to CUSTOM and enables both message toggles. Resolver
tests rewritten per-toggle, including that reading a stranger is never
auto-allowed. Old IF_IN_MY_LIST / TRUSTED_FOLLOWS policies and their strings are
removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Restructure the two TRUSTED_FOLLOWS controls so they read as independent ideas
instead of a confusing read/write sub-toggle:
- The "My relays and people I follow" policy now trusts a followed user as any
counterparty — reading their posts AND reaching them (DM/notification) — plus
your own relays and joined venues. Reading your follows is no longer gated.
- The sub-toggle is repurposed to "Also log in to deliver my messages": trust a
relay to send DMs, replies or notifications to anyone you're talking to, even
people you don't follow.
Resolver: replace servesFollowed{Write,Read}Counterparty with a single
servesFollowedCounterparty, add servesWriteCounterparty (an inbox of anyone
you're messaging), and gate the latter behind the new
messageDeliveryTrustEnabled input. Rename the account setting
relayAuthTrustFollowsForReads -> relayAuthTrustMessageDelivery (+ pref key;
no migration needed, unreleased). The contextual prompt button moves from
read-post prompts to DM/notification prompts and now enables message delivery.
Resolver tests updated for the new inputs, including that the delivery toggle
is write-only and never auto-allows reading a stranger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
The purpose model was person-centric, so an auth-required NIP-28 channel,
NIP-72 community, or NIP-53 live stream broke: reads filter by #e/#a with
no authors, so nothing was attributable -> silent DENY (chat wouldn't
load); top-level posts (no p tags) also silently failed; replies were
mis-attributed as "notify" and trusted on the wrong signal.
Add venue purposes (POST_VENUE / READ_VENUE) carrying the venue id
(channel event-id, or community/live `kind:pubkey:dTag` address). The
deriver recognizes kind-42 channel posts (root `e`), community/live posts
and reads (`#a` 34550/30311), and channel reads (`#e`). Venues are trusted
under TRUSTED_FOLLOWS when you've joined them (publicChatList /
communityList) or their owner — the pubkey in the address, e.g. a live
stream's host — is someone you follow; trusted venues auto-auth for both
reading and posting.
Safety net: any active use we still can't attribute yields an OTHER
purpose so the relay is prompted about instead of failing silently.
Prompt copy gains venue-aware titles/consequences ("Post to this room?",
"If you don't, your message won't be posted."). Default policy already
TRUSTED_FOLLOWS, so joined venues just work. Unit-tested across resolver
and deriver.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Consume the new give-up signal: RelayPublishFailureToastSubscription
(hosted in LoggedInPage) listens for onEventGaveUp and toasts "couldn't
deliver to <relay>", so a dropped send is visible instead of silent.
Rationale polish in the auth settings screen: each relay card now shows
"Last used N ago" and a Forget button that clears both the ALLOW/DENY
override and the accumulated rationale for that relay. The store gains
clearRationale + allLastUsed (default-implemented on the interface) and
records a last-used timestamp on each grant; clearDecision/clearRationale
now prune the shared url key only when a relay has neither an override
nor rationale left, so a partial clear never orphans the reverse-lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Extend RelayAuthPermissionStore with grant rationale (purpose ->
counterparty pubkeys), default no-op so other implementers are
unaffected; DataStoreRelayAuthPermissionStore persists it per relay,
merging counterparties across grants. AuthCoordinator records the
rationale via ledger.recordGrant whenever it authenticates (auto-allow,
override, or approved prompt). The relay-auth settings screen adds a
"Why you're logged in to these relays" section: per relay, purpose-
grouped rows ("Send your private message to:", "Download posts from:")
with each counterparty's avatar + name. Unit-tested: grouping, cross-
grant merge, and that counterparty-less purposes aren't recorded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Introduce the platform-neutral decision core for contextual NIP-42 auth
in commons/relayauth:
- AuthPurpose / AuthPurposeKind / RelayAuthContext describe *why* a relay
wants auth (send DM, deliver notification, read outbox, own relay).
- RelayAuthVerdict adds an ASK outcome (runtime-only; never persisted,
unlike the two-value RelayAuthDecision override).
- RelayAuthResolver is a pure, unit-tested precedence ladder: blocked
list > per-relay override > policy > ASK-if-attributable-else-DENY.
- New TRUSTED_FOLLOWS policy: auto-auth for relays serving a followed
counterparty on a write purpose (DMs/notifications), and — behind a
read sub-toggle — read purposes; strangers fall through to ASK.
Wires the new enum value through the existing settings screen (new
option + strings, reusing the Group symbol) and the URL-only ledger path
(degrades to the my-list check until challenge context is plumbed).
Live prompt UI, grant-rationale persistence, and quartz challenge-context
plumbing are follow-up steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Chat rows render the inner rumor note of a NIP-17 message, but relay
attribution only landed there through narrow windows, so accepted relays
often never showed as icons:
- Duplicate deliveries were stranded on the wrap: a gift wrap re-delivered
by a second relay hit the duplicate branch of consumeRegularEvent, which
tagged the outer wrap note only and never re-processed the event. Extract
the OK-path drilling (wrap -> seal -> rumor) into
LocalCache.addRelayToNoteAndInners and call it from both the OK
confirmation path (markAsSeen) and the duplicate EVENT path, replacing
CacheClientConnector's private copy.
- Cross-thread visibility: Note.event, Note.relays, Note.flowSet and the
innerEventId of GiftWrapEvent/SealedRumorEvent are written by decrypt/index
coroutines and read lock-free on relay socket threads; a stale read parks
an acceptance on the outer envelope permanently. Mark them @Volatile.
- Orphaned UI flows: RenderClosedRelayList/RenderAllRelayList and
createMustShowExpandButtonFlows captured note.flow().relays.stateFlow once
in remember/stateIn; MemoryTrimmingService.cleanObservers destroys the
unobserved NoteFlowSet while the lifecycle is stopped, so resumed rows
never saw another relay update. Wrap in a cold flow that re-resolves
flow() on every collection start.
- Indexing latency: sent DMs waited for the ~1s newEventBundles batcher
before the self-wrap was unwrapped and the message reached the chatroom,
parking early OKs on the wrap. broadcastPrivately and
sendNip04PrivateMessage now run the EventProcessor on the freshly
consumed note immediately; the batched re-delivery is idempotent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEiq1NMK3q12KGQ2yYhPEp
Findings from a full NIP-13 review:
- The spec recommends updating created_at while mining. Queue jobs now
re-stamp the template to "now" when a worker picks them up (a post can
wait behind other jobs, and a job restored after process death could be
hours old); the restorer does the same. Scheduled posts are exempt —
their future created_at is intentional. Anonymous posts re-stamp before
mining against the throwaway key.
- PoWTag.assemble(nonce, null) serialized the literal string "null" as
the third tag entry; a missing commitment now omits the entry entirely.
- New tests: PoWTagTest pins the NIP-13 example tag shape and the
no-commitment round trip; a queue test asserts the created_at
re-stamp at mining start.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Trailing spaces and newlines in a note's content produced empty trailing
paragraphs after the line split, each rendered as a blank FlowRow line
between the last visible word and the end of the component. Trim the
content's tail before segmenting, and return no paragraphs for
whitespace-only content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZET7ig8MvR5vSCSxvcURi
ConcordModeration builds the Control Plane editions for defining a role,
granting roles to a member, and banning/unbanning — each a kind-3308 edition,
plaintext-sealed and wrapped on the Control Plane. It chains the next version
onto the entity's current head (version+1, prevHash=head.hash) and unions the
banlist, taking the community's current editions as input.
Authority is enforced at fold time by the AuthorityResolver, not here: a round-
trip test confirms an owner can define an Admin role, grant it (member gains BAN),
ban/unban (version chaining heals), and that a grant forged by a non-outranking
troll is dropped by the fold.
- ConcordActions.controlEditions opens control wraps into editions
- ConcordCommunitySession exposes controlEditions()/controlPlaneKey() so edits
chain onto live state
- Account.grantConcordRole / banConcordMember / unbanConcordMember publish the
edition with an instant local echo (the session refolds it)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
- Composer chip: replaced the text button with a bolt icon carrying the
difficulty as a small badge (primary-tinted when active, dimmed when
off), matching the layered-icon language of the rest of the options
row; the current choice is bolded in the override menu.
- Mining banner: pulsing bolt while the nonce search runs, per-job
elapsed time driven by a 1 Hz clock ("Note • mining at 20 bits • 12s"),
and proper close IconButtons instead of a text "×". PoWJobState now
carries miningStartedAt for the elapsed display.
- Settings: a live "≈ 45s per post on this device" estimate under the
difficulty picker, from a one-shot cached sha256 benchmark
(PoWEstimator in commons) and the 2^d expected-attempts mean.
- Received notes: DisplayPoW's plain "PoW-24" text is now a compact
secondaryContainer pill (bolt + bits) used by NoteCompose, thread view
and chat messages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Receiving reactions/replies already worked (they decrypt via channelRumors and
wire to their target Note by id). This adds the send side, which must NOT go
through the generic public/NIP-17 reaction path — a plaintext kind-7 would e-tag
the private rumor id onto public relays, and the NIP-17 path wraps to named
recipients, not the channel plane.
- ChannelChat.reaction/reply build kind-7/kind-9 rumors bound to channel+epoch
(reaction e-tags the target; reply q-tags the parent)
- ConcordActions.buildChannelReaction/buildChannelReply wrap them on the plane
- Account.reactToConcordMessage + sendConcordChannelMessage(replyTo) publish the
wrap with an instant local echo, factored through publishConcordWrap
- AccountViewModel.reactToOrDelete intercepts Concord notes (detected by the
ConcordChannel gatherer) and routes to the plane-wrapped reaction
- ConcordChannelScreen wires onWantsToReply into the composer with a reply banner
Zaps already route correctly: a Concord message is an unsigned rumor, so the
existing isPrivateRumor() path forces a PRIVATE (NIP-57 encrypted) zap, same as
NIP-17 DMs. A fully on-plane nutzap (no public receipt) remains a future upgrade.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Backgrounding the app no longer pauses or loses mining:
- commons: PoWPublishQueue checkpoints template jobs through a
PoWJobPersistence hook (saved on enqueue, removed on finish/cancel,
deduped by id so restore is idempotent) and fires onQueueActive on
every enqueue. PoWReplay flattens the live continuation into a
PersistedPoWJob record (broadcast / to-relays / schedule) that can be
replayed headlessly. Opaque enqueueWork jobs (reactions, reposts, gift
wraps) and anonymous posts stay in-memory on purpose — a throwaway key
and its content must never touch disk.
- amethyst: PowJobStore (atomic-rename JSON file, single-lane writer,
3-day staleness purge) and PowJobRestorer, which re-enqueues an
account's checkpointed jobs on login and finishes them via the replay
descriptor. Composers now pass the matching PoWReplay for the public,
group-thread and scheduled paths.
- PowMiningForegroundService: a shortService-type FGS started on every
enqueue (~3 min guaranteed budget, no special permission) that keeps
the process out of the cached-apps freezer while mining. Stops itself
when the queue drains; on onTimeout it exits cleanly since the jobs
are checkpointed. The notification is a live progress card
(NotificationCompat.ProgressStyle): one track segment per post filling
as jobs finish, indeterminate for a single post, per-kind text, a
cancel-all action, and Live Updates rendering on Android 16+.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Follows the NIP-28/NIP-17 model instead of a private per-session flow. The
community session is now purely a decrypt-and-validate gate: each validated
inner rumor is handed to a sink (ConcordRumorSink) that the app wires to
LocalCache.consumeConcordRumor — landing it as a real Note keyed by rumor id
and attaching message-like kinds (9/1111) to the ConcordChannel.
Consequences: messages are first-class Notes, so reactions (kind 7), replies
(1111), deletes (5), OTS and zaps wire up automatically by id through the normal
consume path; the Messages inbox shows real last-message previews and updates
incrementally (filterRelevantConcordMessages, keyed on the ConcordChannel
gatherer) instead of feed() rebuilds; and the chat screen reuses the shared
ChannelFeedViewModel + RefreshingChatroomFeedView (full note UI) with only the
Concord-specific encrypted send path kept bespoke.
- ConcordActions.channelRumors returns validated bound rumors (all chat kinds)
- ConcordCommunitySession/SessionRegistry/SessionManager thread the sink; the
messagesFlow/ConcordChatMessage projection is gone from the session
- LocalCache.consumeConcordRumor attaches gatherer before justConsume so the
inbox incremental filter can route the row
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
Implements user-facing PoW publishing (#3317) on top of the existing
quartz miner:
- quartz: PoWMiner.run gains a cooperative isActive cancellation hook
(checked every ~VALID_BYTES^2 hashes); PoWNostrSigner decorator mines
kind-scoped templates pre-signature so it composes with every signer;
GiftWrapEvent.create/NIP17Factory can mine the outer ephemeral-key
wrap (never the seal/rumor); NostrSignerWithClientTag exposes
prepareTags so mining runs over the final tag set.
- commons: PoWPublishQueue (FIFO, capped worker pool on
Dispatchers.Default, per-job cancel, in-memory only — unmined posts
are lost on process death, logged) and PoWPolicy (kind-group
categories with a hardcoded NEVER list: auth, zap requests, NWC and
bunker RPC, HTTP/Blossom auth, drafts, metadata and lists, OTS).
- amethyst: per-account synced settings (difficulty Off/16/20/24/28 or
custom, per-category checklist) in Compose Settings; Post enqueues the
template and returns immediately; reactions, reposts, reports, private
notes, DMs and long-form route through the same shouldMine gate at
their existing choke points; per-post PoW override chip in the
composer options row; "Mining proof of work… (N in queue)" phase with
per-job cancel in the broadcast banner.
Scheduled posts and anonymous posts mine against the correct key
(scheduled posts skip mining in v1); the client tag is applied to the
template before mining so signing never invalidates the nonce.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V