Commit Graph
534 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub 829994076f Merge pull request #3596 from vitorpamplona/claude/nip-29-compliance-review-ngzzaw
NIP-29: subgroups, custom roles, timeline refs, invite links + relay-signed hardening
2026-07-16 09:37:28 -04:00
Claude 0e8239b22f fix(nip29): audit fixes — subgroup edit safety, subscription load, roles
Bugs:
- Metadata edit could re-root a subgroup or drop its children on a load race:
  the edit ViewModel snapshotted parent/children at prefill and overrode the
  Account-level live-read defaults. Children are no longer snapshotted (Account
  reads the live child list at save time), and the parent is only overridden
  when the user actually re-parents (parentTouched) — a plain rename can't
  re-root or orphan children anymore, even if metadata hadn't loaded yet.
- Parent selector card cached a null channel via remember() and never
  refreshed, so the parent's name/picture never loaded and the warm-up never
  mounted. Now get-or-create + warm + observe the metadata flow.
- previousEventRefs could let a note with an unresolved author slip past the
  self-exclusion and reference the sender's own event. Now requires a resolved
  author.
- Assigning a relay-defined role replaced the member's whole role set while the
  menu implied additive; now keeps existing roles (entry.roles + role.name).
- GroupNAddrInvite now also accepts a bare `invite=<code>` remainder if the `?`
  is stripped upstream (+ test).

Performance:
- Subgroups bar mounted a full warm-up (metadata + content) subscription per
  child chip — up to ~21 relay subscriptions per open group. Replaced with one
  relay-directory subscription; chips read from cache.
- Parent picker recomputed the whole candidate scan on every recomposition
  (each search keystroke) via a produceState initial-value argument; the scan
  now lives only in the producer with a cheap empty initial.

spotless clean; quartz tests green; amethyst compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qst2JsmNYMvXitv2vxo4S
2026-07-16 04:19:25 +00:00
Claude c236474f88 feat(nip29): timeline refs, custom roles, subgroup nav, naddr invites
Follows up the subgroup protocol work with four NIP-29 compliance/UX gaps.

previous timeline references (spec §Timeline references):
- RelayGroupChannel.previousEventRefs(): first-8-char id prefixes of the most
  recent events seen from the host relay, excluding the sender's own, capped at
  the spec's 50-event window. Only draws from events actually received in the
  channel so the host relay is known to have them.
- Populate the `previous` tag on all outgoing group events: kind-9 chat and
  replies, kind-1111 minichat comments, kind-11 threads, and group replies.

Custom roles (kind 39003):
- Route SupportedRolesEvent onto the channel (LocalCache.consume + a
  RelayGroupChannel.supportedRoles field) instead of only storing it.
- Members screen: when the relay advertises a role set, offer those roles when
  assigning (admins), and show each member's real relay-assigned role label
  instead of collapsing everything to admin/moderator. Falls back to the
  built-in admin/moderator shortcuts when no 39003 is published.

Subgroup navigation (spec §Subgroups):
- RelayGroupSubgroupsBar: a self-hiding bar under the pinned bar showing a
  breadcrumb up to the parent group and chips for the child subgroups (in the
  relay's `child` order), each opening that group on the same host relay.

naddr invite codes (spec §Group identifier):
- GroupNAddrInvite parses the `naddr1…?invite=<code>` suffix; both the tap
  handler (ClickableRoute) and the deep-link handler (MainActivity) now feed it
  into the kind-9021 join request so a shared invite naddr auto-joins.

Tests for the naddr invite parser. spotless clean; quartz tests green;
amethyst compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qst2JsmNYMvXitv2vxo4S
2026-07-16 02:29:10 +00:00
Vitor PamplonaandGitHub 99f28ee1ca Merge pull request #3593 from vitorpamplona/claude/nip-51-compliance-review-vwhyvn
Add hashtag muting support via NIP-51
2026-07-15 22:07:00 -04:00
Claude abc6732286 feat: support NIP-51 mute-list hashtag ("t") entries
NIP-51's kind:10000 mute list defines four entry types — `p` (pubkeys),
`word`, `e` (threads) and `t` (hashtags). Quartz parsed only the first
three, so `t` hashtag mutes written by other clients were silently
dropped: uncounted, invisible, and never applied to filtering.

Quartz:
- Add HashtagTag (`"t"`) implementing the MuteTag sealed interface, and
  register it in MuteTag.parse/isTagged so it round-trips like the other
  entry types.
- Add mutedHashtags()/mutedHashtagIds() TagArray helpers.

Filtering (commons):
- Add hiddenHashtags to LiveHiddenUsers plus isHashtagHidden(), and hide
  notes carrying a muted hashtag in Note.isHiddenFor() (exact, case-
  insensitive `t`-tag match — distinct from the existing substring word
  scan).

Amethyst:
- Aggregate HashtagTag entries from the mute/block lists in
  HiddenUsersState.
- MuteListState.hideHashtag/showHashtag + Account and AccountViewModel
  wrappers, and observeUserIsMutingHashtag.
- Surface a Mute/Unmute hashtag action in the hashtag screen's options
  overflow menu.

Tests: HashtagTagTest (parse/round-trip/MuteTag dispatch) and
NoteIsHiddenForTest cases for muted-hashtag hiding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017giudm3gXumsxZmd3uMQc8
2026-07-16 01:51:48 +00:00
Claude e24eec9ca6 feat(nip29): subgroup hierarchy (parent/child) for relay groups
Implements the NIP-29 Subgroups feature merged upstream: groups can now be
organized into a parent/child tree, scoped per host relay.

Quartz:
- Add `parent`/`child` tag classes and TagArray (builder) helpers.
- GroupMetadataEvent (39000): parent()/children()/isRoot() accessors and
  build params.
- EditMetadataEvent (9002): parent()/children() accessors and build params
  (a 9002 re-carries the full child list, per spec, or the relay rejects it).
- SubgroupTree: assembles a relay's flat 39000 set into the hierarchy —
  structure follows each group's parent tag, sibling order follows the
  parent's child-tag order, orphans surface as roots, and malformed cycles
  are broken rather than looping.
- NIP-11: advertise/detect subgroup support via `nip29: { subgroups: true }`,
  with a `subgroups()` builder DSL helper.
- Tests for tag round-trips, tree assembly, ordering, orphans and cycles,
  plus NIP-11 serialization.

Amethyst:
- RelayGroupChannel: parentGroupId()/childGroupIds()/isSubgroup() reading the
  latest metadata.
- Account.editRelayGroupMetadata: preserve the group's current parent and full
  children list on a plain metadata edit so an admin renaming a subgroup no
  longer detaches it (or gets rejected for dropping children).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Qst2JsmNYMvXitv2vxo4S
2026-07-16 01:51:40 +00:00
Claude ae9f4a0def fix(concord): resolve invite coordinate per CORD-05 (honor revocation)
Follow-up making the invite redeemer match the CORD-05 §2 spec for the
addressable invite coordinate (33301, link_signer, d=""):

- vsk=6 → live bundle (open with the link token)
- vsk=9 → revocation tombstone: the newest event wins, so a tombstone buries
  even a stale, still-openable copy on another relay ("a fetcher finds the
  grave instead of keys"). Amethyst previously never checked for this, so a
  revoked link failed generically.
- anything else present (e.g. a mis-posted registry vsk=8, the shape of the
  relayop.xyz link that hung) → unreadable
- nothing on any relay → absent

New pure `ConcordInviteBundle.classify(wraps, token): InviteBundleStatus` in
quartz (next to parse/validate), wrapped by `ConcordActions.classifyInvite`,
and mapped by `Account.joinConcordViaInvite` to the `ConcordInviteResult`
cases — including a new `Revoked` outcome with its own message and no futile
retry. Crypto is unchanged and already matches the spec
(hkdf(token,'concord/invite-key') → NIP-44 → snake_case CommunityInvite).

Adds ConcordInviteClassifyTest covering live / revoked (order-independent) /
unreadable / absent, plus the real relayop.xyz vsk=8 event → Unreadable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KngFNwrQDLYa9QW1f5RRD
2026-07-16 01:24:37 +00:00
Claude 9ee6f0dbde feat(nip29): message pinning for relay groups
Implements NIP-29 message pinning (nostr-protocol/nips#2379):

Protocol (quartz):
- GroupPinnedEvent (kind 39005): relay-signed pinned-message list, `d`
  group id + ordered `e` ids.
- UpdatePinListEvent (kind 9010): moderator `update-pin-list` write,
  carries the full list so pin/unpin/reorder/clear are one submission.
- Register both in EventFactory; add a pinnedEventIds tag helper.

Model + cache:
- RelayGroupChannel now folds the pin list (pinnedEventIds / isPinned)
  with the same createdAt-supersede guard as the roster.
- LocalCache consumes 39005 into the channel and stores the 9010 write;
  39005 added to the group's metadata REQ filter so pins load.

Publish path:
- Account.pin/unpin/updateRelayGroupPins + AccountViewModel wrappers.

UI (non-intrusive, self-hiding):
- Collapsed pinned-message bar under the top bar: shows the current pin,
  N-count cycling, tap to jump to the message in-feed (hoisted jump
  request threaded through the shared chat feed view). Renders nothing
  when the group has no pins.
- Moderator-only Pin/Unpin action under "Show more" in the chat message
  bottom drawer, gated on membership.canModerate().
- Small pin glyph on pinned bubbles' footer.

Tests: quartz build/parse round-trip for both kinds; channel pin-fold
supersede/replace/clear semantics.
2026-07-15 21:24:37 +00:00
Claude bb9f0c0cb3 fix(concord): render imeta-only chat images and send thumbhash
Concord/NIP-C7 image messages from interop clients (e.g. Ditto/Soapbox
Armada) attach the image purely as a NIP-92 `imeta` tag and leave the
message content empty. Amethyst's shared renderer only shows media whose
URL appears in the text, so those images never rendered — even though the
encrypted blob's key/nonce were already registered for transparent
decryption at ingest.

RenderChat now appends any imeta URL missing from the content before
handing it to the media renderer (symmetric to ChannelChat.imageMessage,
which appends the URL on send), so imeta-only attachments display.

Also forward the thumbhash on the encrypted-image imeta: FileHeader
computes both blurhash and thumbhash, but only blurhash was being sent.
encryptedImageImeta now emits an additive `thumbhash` field and
toConcordImeta forwards the computed value, so receivers can paint a
placeholder while the blob decrypts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TGjBiJL2fvTR37eePBKrG
2026-07-15 14:54:48 +00:00
Vitor PamplonaandGitHub 7680dfbce6 Merge pull request #3568 from vitorpamplona/claude/marmot-group-icons-tlcwa1
Add MIP-01 v2 group avatar encryption and upload support
2026-07-15 08:14:25 -04:00
Claude a47fd206e6 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt
#	cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt
2026-07-15 02:12:18 +00:00
Claude 3950323ba3 fix(concord): audit fixes — memory, folding, concurrency, notifications, UI
Address the deep-audit findings across the new Concord code:

- H1: stop persisting kind-21059 ephemeral typing wraps as durable notes.
  EphemeralGiftWrapEvent extends GiftWrapEvent, so every heartbeat was
  stored forever; drop it once the session has ingested it.
- H2: follow()/unfollow() now read the offline backup (entriesWithBackup),
  so a join racing the async backup load can no longer wipe the joined list.
- M1 (banlist): fold to the head (honors a chained unban) then union in
  authorized editions that aren't ancestors of the head — concurrent bans
  are healed without resurrecting an on-chain unban (CORD-06 down-only).
- M2: reproject only the newly-arrived channel wrap incrementally instead
  of re-decrypting the whole buffer per message (was O(n^2)); refold only
  projects newly-folded channels.
- M3: cancel a session's old state-watcher before replacing it on a
  Refounding rebuild (was a coroutine + session leak per rekey).
- M4: publish typing/state/members/observed-authors under the lock and
  make revision/observedAuthors updates atomic; clamp future-dated typing.
- M5: notification Concord bypass now requires the community to be one this
  account has currently joined (mirrors the Marmot guard).
- M6: Concord chat honors the "Messages in notifications" toggle.
- L1: carry NIP-30 emoji tags on minichat replies, image captions and
  custom-emoji reactions.
- C1: make the composer VM init() idempotent so recomposition can't wipe a
  picked image or an open suggestion list.
- C2/C3: ConcordHome channel rows and unread badges react to the channel's
  own notes flow instead of the global revision (no stale rows / flicker).
- C4: try/finally around mint-invite / create / save so a thrown call can't
  strand the button disabled.
- C5: gate the typing ticker on active heartbeats so an idle channel stops
  waking a 2s loop.

Adds regression tests for concurrent-ban union-heal and unauthorized bans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-15 00:37:13 +00:00
Claude de2e71dad0 fix(marmot): match mdk/whitenoise MIP-01 v2 group-image scheme for interop
Verified against the mdk-core revision whitenoise-rs pins (marmot-protocol/mdk
@e8cd584): its NostrGroupDataExtension parser consumes name/description/admins/
relays/image_hash/image_key/image_nonce/image_upload_key and rejects ANY trailing
bytes at a known version, and its extension/group_image.rs fully implements avatar
encryption. The previous "canonical raw-key + media_type" approach both (a) added a
trailing media_type field that mdk rejects — breaking the whole group for whitenoise
members — and (b) used a key scheme mdk can't decrypt.

Re-implement to mdk's exact MIP-01 v2 scheme so avatars interoperate byte-for-byte:
- image_key / image_upload_key are HKDF seeds (reusing Mip01ImageCrypto's
  mip01-image-encryption-v2 / mip01-blossom-upload-v2 labels; HKDF-SHA256 with empty
  salt == mdk's Hkdf::new(None, seed)). AEAD key derived from the seed.
- ChaCha20-Poly1305, 12-byte nonce, EMPTY AAD, image_hash = SHA-256(ciphertext).
- Decrypt tries v2 (HKDF) then falls back to v1 (raw key), exactly like mdk.
- Remove media_type from the wire entirely (and from the model/cipher/uploader), so a
  v2 image extension ends at image_upload_key with zero trailing bytes. The plaintext
  MIME isn't stored; the display path lets Coil sniff the format.
- Derive the Blossom upload keypair from image_upload_key instead of storing a raw key.

Adds a regression test that reproduces mdk's v1/v2 field consumption and asserts a
v2 image extension has no trailing bytes, plus a test pinning the HKDF-seed + empty-AAD
scheme so future drift from mdk is caught.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JL3GXW1fmHa3xWfQjLLqfp
2026-07-15 00:32:32 +00:00
Claude 339d7c10e7 feat: Marmot group icons — canonical encryption, feed display, metadata editing
Add first-class support for Marmot (MLS-over-Nostr) group avatars.

Protocol (quartz):
- Implement the canonical `marmot-group-image-v1` scheme: raw ChaCha20-Poly1305
  key + 12-byte nonce, AAD = "marmot-group-image-v1" || 0x00 || media_type,
  image_hash = SHA-256(ciphertext). MarmotGroupImageEncryption emits canonical
  and decrypts both canonical and the deprecated MIP-01 HKDF-seed scheme.
- Add the `image_media_type` field to MarmotGroupData as a trailing TLS field
  (older readers ignore it; disappearing_message_secs stays positionally
  unambiguous). Add withImage/withoutImage helpers.
- MarmotGroupImageCipher (NostrCipher) drives both encrypted upload and
  transparent decrypt-on-download.

Model/manager (commons):
- MarmotGroupChatroom exposes an `image` StateFlow; MarmotManager.syncMetadataTo
  populates it from the group metadata.

Android:
- Show the decrypted group icon in the Messages feed; when a group has no image,
  fall back to the NIP-11 icon of one of its relays (fetched on cache miss).
- Group metadata editing gains an icon picker (add/change/remove) in both the
  create and edit screens; create also gains a description field. Icons are
  encrypted and uploaded to Blossom via UploadOrchestrator, signed with a fresh
  per-image keypair stored as image_upload_key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JL3GXW1fmHa3xWfQjLLqfp
2026-07-14 23:17:34 +00:00
Vitor PamplonaandClaude Opus 4.8 92f422be88 fix(concord): authority-gate the control-plane fold (CORD-04 §1)
The fold selected each entity's head by STRUCTURAL fold only and never dropped
editions from unauthorized signers, so a spoofed edition that structurally
supersedes a legit one won — on the live Soapbox community this surfaced a decoy
metadata edition ("invalid ... clients should be ignoring this", no icon) over
the real owner-authorized one, so no community icon/banner ever rendered.

Two stacked wire/authority bugs, both fixed:

- RoleEntity.scope was typed String, but the reference client (Armada) writes it
  as an object ({"kind":"server"} / {"kind":"channel","channel_id":...}) per
  CORD-04 §2. The type mismatch failed the whole RoleEntity decode, dropping the
  role — and with it every grant depending on it — so no admin ever resolved.
  Added RoleScope and retyped the field (no writer set it, so no migration).

- AuthorityResolver.resolve now folds each role/grant/banlist CHAIN through
  authorized editions only, via an owner-rooted fixpoint over the full edition
  set (not the post-hoc structural heads). A rogue higher-version grant from an
  unprivileged key is dropped instead of superseding the owner's grant, so the
  legit authority stands. ConcordCommunityState.fold then gates metadata by
  MANAGE_METADATA, channels by MANAGE_CHANNELS, the banlist by BAN, and the
  dissolution tombstone to the owner alone.

Verified live end-to-end: `amy concord channels Soapbox` now folds the real
metadata (name "Soapbox Community", the blossom icon + banner pointers) and all
9 channels; fetching + AES-256-GCM-decrypting the icon pointer yields a
hash-verified PNG. Regression tests cover the object-scoped role and the rogue
superseding grant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandClaude Opus 4.8 a4d9087187 fix(concord): include the 32-zero id in the invite bundle key (CORD-05)
CORD-05 Appendix A.1 says the HKDF `id` is always present — 32 bytes,
all-zero for a label with no meaningful id — and A.6 lists
`concord/invite-key` with `id = 0…0`. inviteBundleKey built its info with
no id at all, so it derived a different bundle key than the reference
client (Armada) and NIP-44 decryption of an Armada-minted invite bundle
failed with "Invalid Mac" — the join aborted with "no valid bundle for
this link". amy round-tripped with itself (same wrong key both sides), so
its own tests passed and the divergence went unnoticed.

Verified by decrypting a real Armada invite bundle: the no-id key fails the
MAC; the zero-id key yields a valid CommunityInvite JSON. Pass ByteArray(32)
like the sibling banlist/dissolved derivations already do.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandClaude Opus 4.8 8060dc0bb1 fix(concord): fold refounded community control plane for fresh joiners
After a Refounding (CORD-06 §3), compaction re-wraps each entity's head
verbatim, so a head edited past genesis still carries an `ep`/prev citing
an edition in the prior epoch that a fresh joiner never fetches.
EditionFold.foldEntity required a genesis edition (prevHash == null) and
returned null otherwise, so a fresh login folded the whole Control Plane to
nothing: no community icon, no name, no edited channels (while Armada,
which implements the CORD-04 §1 fresh-joiner rule, showed them all).

Anchor at the lowest-version edition when no genesis is present and walk up
from there. Safe: Amethyst always re-folds the whole buffer from scratch
(no persistent floor), so it is structurally always a fresh joiner;
authority is validated on top by AuthorityResolver, so an unrooted forgery
is still dropped; and a genuine mid-chain gap still stops at the intact
prefix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Claude 58e018c877 feat(cli): graperank scores always persist locally; add publish + rank verbs
Every `amy graperank` / `graperank score` run now reconciles its result into
the local event store as NIP-85 kind:30382 cards signed by the per-observer
service key (cutoff --min-rank, default 2): changed ranks are re-signed,
unchanged ones skipped (no event-id churn), dropped targets retracted with a
kind:5 the store applies on insert. The store is the source of truth, so the
score of a run is durable and reusable instead of ephemeral stdout.

New verbs complete the crawl -> score -> publish pipeline:
- `graperank publish [OBSERVER] [--relay URL[,URL...]]` — transport only:
  one NIP-77 up-only reconcile per operator relay over the provider key's
  kind:30382 + kind:5, converging the relay to the local set (deletions
  propagate, lost cards restored, full-set publish fallback for relays
  without negentropy); also refreshes the observer's kind:10040 pointer.
- `graperank rank USER [--provider P] [--refresh]` — the consumer side:
  newest card per provider from the local store, with a relay drain on miss.

GrapeRankPublisher (quartz) is reworked accordingly: reconcileAndPublish is
replaced by reconcileLocal (sign+insert+retract against the store) and
syncToRelays (NegentropyStoreSync up-only + blastPublish fallback), with a
commonTest covering upsert/skip/retract and re-carding a retracted target.

BREAKING (--json / flags): `--publish`, `--publish-limit`, `--publish-relay`
and `--bench-sign` are removed from the score command. Its JSON drops
published/publish_rejected/deleted/delete_rejected/skipped_unchanged/
publish_truncated/published_kind/published_to/publish_error/observer_10040/
bench_signed/bench_sign_ms and gains provider_pubkey/min_rank/cards_total/
cards_signed/cards_unchanged/cards_retracted/cards_ms. Publishing moved to
the new `graperank publish` verb.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
2026-07-14 20:29:09 +00:00
Claude e734d6400c feat(concord): send & receive encrypted image messages (Armada-compatible)
Concord channel messages can now carry images, wire-identical to Soapbox
Armada's `encryptAttachments`: a normal channel-bound kind-9 whose ciphertext
URL is appended to the content and annotated by a NIP-92 `imeta` tag with
`encryption-algorithm aes-gcm`, hex `decryption-key`/`decryption-nonce`, and the
plaintext `ox` hash (no `x`). The blob is AES-256-GCM ciphertext on Blossom, so
the media host and relays only ever see encrypted bytes — the community's E2E
guarantee holds.

Reuses the NIP-17 encrypted-media stack end to end: quartz's imeta tag vocab
and IMetaTagBuilder to build/parse the tag (ChannelChat.imageMessage /
encryptedImageImeta / encryptedImagesOf), the shared UploadOrchestrator
encrypted upload + ChatFileUploadDialog picker on the send side, and the OkHttp
EncryptedBlobInterceptor keyCache on the receive side — registering each
attachment's cipher (keyed by URL) lets the normal feed renderer display the
decrypted image with no shared-render changes. Encryption is mandatory
(no toggle, and a missing cipher fails closed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 19:44:56 +00:00
Claude e231b0cf5b Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-14 15:25:40 +00:00
Claude 103a3dfc23 fix: skip event parse of community approval contents that are not JSON objects
Clients in the wild publish kind-4550 approvals whose content is plain
text (e.g. braille ASCII art) or a custom non-event JSON wrapper instead
of the NIP-72 stringified approved event. containedPost() fed any
non-blank content straight into Event.fromJson, producing a
JsonParseException and a noisy logcat warning for every such approval.

Only attempt the parse when the trimmed content starts with '{', so
plain-text contents are skipped silently while genuinely malformed
event-looking JSON still logs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXiy3Ajmr3vdrV83jZsUx6
2026-07-14 14:41:43 +00:00
Claude d29ee4e6a6 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-14 04:27:53 +00:00
Claude bed5d93a12 feat(concord): typing indicators (CORD kind 23311)
Publish a kind-23311 typing heartbeat as an ephemeral (21059) stream wrap
on the channel plane, throttled to once every few seconds while composing.
The session folds inbound heartbeats into a per-channel typing map with an
8s freshness window (never echoing the local user), and the channel screen
renders a slim "X is typing…" line above the composer with a ticker so a
typist who stops silently fades out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-13 23:21:01 +00:00
Vitor PamplonaandClaude Opus 4.8 28b93132a3 feat(concord): read + render CORD-02 §6 encrypted community icon/banner
Concord community icons never showed (robohash instead), and the community
name silently fell back to the invite name. Root cause: the icon/banner are
CORD-02 §6 **encrypted media** — the metadata entity carries an
`ImagePointer` object `{url,key,nonce,hash}` (AES-256-GCM ciphertext at
`url`, decrypted with `key`/`nonce`, `hash` = SHA-256 of the plaintext) —
but `MetadataEntity.icon` was typed `String?`. An object where a String is
expected fails the whole entity's decode, so metadata came back null: no
icon, and the name dropped to the entry fallback. Matches the Concord v2
reference client (Armada `concord-v2/lib/{types,image}.ts`).

- Promote `ImagePointer` to a shared CORD-02 type (was invite-only) and give
  it `decryptOrNull` (AES-256-GCM via the existing `AESGCM`, verifying the
  plaintext SHA-256 — a swapped blob fails closed).
- `MetadataEntity.icon`/`banner` are now `ImagePointer?`, so the entity (and
  the community name) decodes. `ConcordChannel` carries the pointers.
- `rememberConcordImageModel` resolves a pointer for the avatar: a plain-URL
  pointer (Amethyst's own form) passes through; an encrypted one is fetched,
  decrypted, verified, cached to disk, and rendered — else the robohash. Wired
  into the Concord hub avatars and the Messages-tab community chip.
- Amethyst's create/edit still take a URL and wrap it as a url-only pointer;
  authoring encrypted images (encrypt + upload) is a follow-up.

Adds ImagePointerTest: Armada-shape object decode, decrypt round-trip, and
fail-closed on a tampered hash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:16:12 -04:00
Claude 648e9f427a feat(concord): wire CORD-06 refounding for real member removal
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
2026-07-13 12:58:36 +00:00
Claude 184f0bfde7 refactor: align ContactCardEvent with the nip88Polls class structure
Replicates the PollEvent layout: all tag parsing moves to TagArray extensions
in TagArrayExt.kt with the event accessors delegating to them, builder
extensions use addUnique for single-instance tags, and construction goes
through template-returning builders instead of methods that sign internally.

- build() returns an EventTemplate<ContactCardEvent> (the signer is only used
  to NIP-44 encrypt the private tags, matching TrustProviderListEvent);
  create() remains as the signer.sign(build(...)) convenience and now takes
  the emoji list directly
- updatePetNameAndSummary() returns an unsigned EventTemplate; callers sign
  it (ContactCardsState and tests updated)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 12:28:23 +00:00
Claude ebeecd1ee7 feat: custom emojis in contact card petnames
Nicknames can now use NIP-30 custom emojis: typing : in the nickname dialog
autocompletes from the account's emoji packs, and the emoji mappings for any
shortcode used are embedded in the card's NIP-44 encrypted content next to the
petname — so even the emoji set stays private. Renderers resolve the petname's
shortcodes against the card's decrypted tags instead of the profile's metadata
tags.

- quartz: updatePetNameAndSummary replaces the private emoji tag set wholesale
  and keeps it out of the public tags; round-trip test added
- commons: PetName(name, tags) holder with content equality, decryption cache
  returns the merged decrypted tag list, EmojiPackState.findEmojiTags resolves
  :codes: against the selected packs
- amethyst: Account embeds resolved emoji tags on save; all petname render
  sites pass the card tags to the WithEmoji composables; nickname dialog gets
  the : emoji autocomplete via EmojiSuggestionState

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 01:39:59 +00:00
Claude 8ee90907ca feat: nickname users via NIP-85 contact cards signed by the account key
Adds petnames/nicknames per https://github.com/nostr-protocol/nips/pull/761,
reusing the kind:30382 contact card already used for WoT scores — a card only
acts as a nickname when signed by the account's main key. Petname and summary
are always stored in the card's NIP-44 encrypted content.

quartz:
- ContactCardEvent.updatePetNameAndSummary edits both fields in the encrypted
  private tags, strips stray public copies, preserves every other tag
- TagArray.petName()/summary() parsers for decrypted tag lists + tests

commons:
- ContactCardDecryptionCache: LRU NIP-44 decrypt cache for own cards
- ContactCardsState: account-scoped access to the account's own cards,
  petname flow per target user, create/update entry point

amethyst:
- Account.updateContactCardPetName publishes through the extended outbox
  relays (NIP-65 write + private outbox + local + broadcast) and the login
  subscription now downloads the account's own kind:30382s from its relays
- petname renders instead of the display name in usernames, profile header,
  chats and @mentions (observeUserPetName)
- Edit-nickname action + dialog on the profile actions menu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 01:26:27 +00:00
Claude 025d63672f feat(chat): dual-mode replies (inline + minichat) — foundation, Concord composer, shared chip
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
2026-07-12 20:40:29 +00:00
Claude 3cac9077ec refactor(concord): delete ConcordKinds; kinds live on their owning classes
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
2026-07-11 15:12:32 +00:00
Claude 9c71034dd2 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-11 15:04:57 +00:00
Claude 0015b39085 fix(concord): read/write the kind-13302 list in Armada's wire format
Amethyst serialized the joined-community list (kind 13302) as a bare JSON
array of flat camelCase entries, so it could not parse the document Soapbox
Armada actually publishes — a community created in Armada never appeared in
Amethyst even after the 13302 reached a shared relay.

Rewrites the codec to Armada's communityList.ts shape:
{ entries: [ { community_id, seed: JoinMaterial, current: JoinMaterial,
added_at } ], tombstones: [ { community_id, removed_at } ] }, where
JoinMaterial is the snake_case per-snapshot key bundle (community_id, owner,
owner_salt, community_root, root_epoch, channels[], relays, name, held_roots?,
refounder?). Hydration prefers current over seed; liveness is derived from
tombstones (an entry is dropped only when removed strictly after it was added).
New create/join entries now stamp added_at (ms) so the liveness tiebreak works.

Adds interop tests that decode a real Armada document and exercise the
tombstone-after-add drop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-11 14:56:30 +00:00
Claude 127d959c8d refactor(concord): community-list, control, invite-bundle events own their kind literals
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
2026-07-11 14:56:28 +00:00
Claude 0e1a14a62d fix: don't drop whole kind-0 profiles over one mistyped field
Clients in the wild publish structurally wrong values in profile
metadata — e.g. "nip05":{} — and the strict field serializers made
JsonMapper throw, so contactMetaData() returned null and the entire
profile (name, picture, about…) was discarded.

Generalize the BirthdayTolerantSerializer precedent: string fields now
use TolerantStringSerializer (accepts any JSON primitive, ignores
objects/arrays/null) and the bot flag uses TolerantBooleanSerializer,
so a single malformed field is skipped instead of being fatal.

Non-JSON content (e.g. "Relay initialized") remains unrecoverable and
still parses to null; a test pins that it does so without throwing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSCGp6KgcnaepEMbxNroYe
2026-07-11 14:34:50 +00:00
Claude ac98036a8e perf(pow): mine NIP-13 proof of work on all cores and drop per-hash allocations
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
2026-07-11 04:04:28 +00:00
Vitor PamplonaandGitHub 5c8801082a Merge pull request #3513 from vitorpamplona/claude/amy-graperank-crawl-perf-jhmx0x
GrapeRank crawl: connect once, wait once — 38% faster at hop-3; hop-8 31% faster at +15% lists / +47% users
2026-07-10 23:19:58 -04:00
Claude 8326f08f43 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-11 01:51:00 +00:00
Vitor PamplonaandGitHub e60003063b Merge pull request #3527 from vitorpamplona/claude/nip13-pow-publishing-queue-1takp1
Add NIP-13 Proof-of-Work mining queue with persistence and UI
2026-07-10 21:48:03 -04:00
Claude b48cc2c117 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt
2026-07-10 22:53:52 +00:00
Claude d1ba04ad52 fix(relayauth): pre-merge audit — auth retry budget + lost-prompt window
Two bugs found in a pre-merge audit:

- quartz PoolEventOutboxState: auth-required NAKs only spared the `responses`
  budget, but `tries` (grown by every send/re-pump and NOT auth-aware) still
  accumulated across reconnects, so a slow/flapping AUTH handshake could exhaust
  Tries.isDone() and drop the event — with a spurious onEventGaveUp — before AUTH
  landed. Now an auth-required NAK resets the relay's retry budget (it responded,
  so it's up and just wants auth). Regression test added.

- RelayAuthPromptBus used a replay=0 SharedFlow, so a challenge that resolved to
  ASK before RelayAuthPromptHost subscribed (cold start / account switch) was
  dropped and the auth coroutine stalled the full timeout then DISMISSed. Add
  replay so late subscribers recover pending prompts (the host already filters
  resolved ones). Regression test added.

Also record the as-built design (Always/Never/Custom + toggles, venues, give-up
toast, known deny-relay-outbox limitation) in the plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:13 +00:00
Claude 9179bd9d8d feat(quartz): signal when an outgoing event exhausts its retry budget
Instead of silently dropping an event once its per-relay retry budget is
spent, PoolEventOutbox now reports it: PoolEventOutboxState.newTry returns
whether the attempt gave up on the relay, PoolEventOutbox.onSent surfaces
the dropped event, and NostrClient notifies a new (default no-op, so
non-breaking) RelayConnectionListener.onEventGaveUp(relay, event). Lets a
host surface a failed delivery rather than lose it silently; the event may
still be pending on other relays. Unit-tested via the outbox try budget.

Note: timed retry backoff (the other half of this item) is intentionally
deferred — applied in the shared syncState path it would also delay the
post-auth resend and regress the auth-required fix, so it needs
trigger-aware handling designed separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:10 +00:00
Claude 38b2e65362 feat(quartz): expose pending outbox events per relay for auth context
Add INostrClient.activeOutboxEvents(url) (backed by
PoolEventOutbox.activeOutboxEventsFor) returning the full events still
pending delivery to a relay, not just their ids like activeOutboxCache.
This lets a host explain *why* a relay is being authenticated with —
e.g. a pending kind-1059 gift wrap means we're sending a DM to its
recipient — by inspecting kind/tags. Combined with the existing
activeRequests(url) filters, it is the generic challenge context the
NIP-42 decision hook needs. Updates the INostrClient test fakes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 9c618f9ba1 fix(quartz): don't drop outgoing events on NIP-42 auth-required NAKs
PoolEventOutboxState treated an `auth-required` OK-false the same as any
transient failure, recording it against the per-relay Tries budget
(responses > 2 drops the event on the next send attempt). Relays that
NAK every unauthenticated EVENT could therefore exhaust the budget and
drop the message before the AUTH handshake completed — the event was
gone by the time syncFilters re-sent it after the auth OK.

Treat `auth-required` as a deferred state instead: keep the relay in
relaysRemaining and record no failure, so the existing
syncFilters-after-auth path redelivers it. Mirrors the behavior already
present in StandaloneRelayClient. Terminal rejections (invalid/pow/
replaced/deleted) and ordinary transient errors are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
2026-07-10 22:40:09 +00:00
Claude 97fc801294 fix: NIP-13 compliance — refresh created_at at mining start, clean nonce tag
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
2026-07-10 22:22:04 +00:00
Claude 8988c98308 feat: add NIP-13 proof-of-work publishing with a fire-and-forget mining queue
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
2026-07-10 20:24:45 +00:00
Claude 88732d507c feat(concord): make joined-list a registered Event (13302) for cache/account use
Promote the kind-13302 joined-communities list to a proper Event subclass so the
Android LocalCache/Account layer can observe it the way RelayGroupListState
observes SimpleGroupListEvent (10009):

- ConcordCommunityListEvent : Event — createAddress (13302, pubkey, ""), a
  suspend create(signer, entries), and decrypt(signer); content stays NIP-44
  self-encrypted and carries each community's secrets
- register the kind in EventFactory so inbound events parse to the typed class
- extract encode/decode JSON helpers in ConcordCommunityList (shared by both)

Test: create -> the wire form hides the name, a JSON round-trip resolves to the
typed class via EventFactory, decrypt recovers entries, and the replaceable
address is (13302, pubkey, ""). Green on :quartz:jvmTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-10 17:36:49 +00:00
Claude 743658433f Merge remote-tracking branch 'origin/main' into claude/amy-graperank-crawl-perf-jhmx0x
# Conflicts:
#	cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt
2026-07-10 17:12:10 +00:00
Claude 51494d5330 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-10 17:05:56 +00:00
Claude 2bad6779ce perf(graperank): mass pre-connect, cached DNS, and an amy relay census probe
Most of a from-scratch crawl's wall clock was connection setup, re-paid
serially: the relay pool tears down a relay's socket ~300ms after its last
drain unsubscribes, OkHttp allowed only 256 concurrent WS handshakes, and
every dial re-ran an uncached blocking getaddrinfo (10-30s per dead domain,
once per per-user path URL of the same host). Open the connections once, in
parallel, and keep them:

- GrapeRankCrawler: the warm-pool trick (never-matching REQ that only holds
  the socket) now covers the whole candidate universe instead of the top 20 -
  seeded at crawl start from the reachability cache's live set (one parallel
  connection storm, Config.knownLiveRelays) and refreshed each round with
  newly learned outbox relays, capped by Config.preconnectCap (FD-budget
  aware, --preconnect-cap / --no-preconnect).
- CachingDns (quartz jvmAndroid): 10-min positive + negative DNS cache with
  in-flight per-host dedup; dead domains fail in microseconds instead of
  re-burning resolver timeouts, path URLs of one host resolve once.
- cli Context: dispatcher and pre-connect caps derived from the process's
  open-files limit (UnixOperatingSystemMXBean), warning when ulimit is low.
- amy graperank probe: relay census - mass-connects every relay the store
  knows (kind:10002 universe deduped per authority + cached verdicts) in
  waves, records live/dead with real measured rtt-open into the NIP-66
  reachability cache (RelayProber + RelayReachabilityStore.recordProbed),
  so the next crawl skips dead relays and waits once for the slow-but-alive.

Single-server limits (FDs, ephemeral ports, DNS, threads, conntrack) and the
design are documented in quartz/plans/2026-07-10-graperank-connect-storm.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zEYRGKF943RgLaHTViJaB
2026-07-10 01:13:23 +00:00
Claude 46a2652fc5 feat(concord): add direct invites (kind 3313)
Completes CORD-05: for a known npub, deliver the CommunityInvite as a NIP-59
giftwrap instead of a public bundle — a kind-3313 rumor sealed (kind 13) to the
recipient and wrapped (1059) with ["p", recipient] and a ["k","3313"] index tag
so recipients can query pending invites without decrypting every giftwrap.
Cannot be revoked (recipient holds the keys on arrival).

Reuses SealedRumorEvent + the giftwrap primitives. Tests cover round-trip to the
intended recipient (with p/k tags) and that strangers cannot open it. Green on
:quartz:jvmTest.

With this, the Quartz protocol layer covers CORD-01..07 end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-09 23:11:22 +00:00