The per-user outbox retry bound doesn't need to be tunable — replace the
--max-attempts flag with a MAX_OUTBOX_ATTEMPTS = 3 constant. Same behaviour,
one fewer knob. Updates usage text, README, and the parity doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
Replace the depth-limited, user-capped BFS with a completeness loop that runs
until every discovered user's kind:10002 outbox has been checked and their
latest kind:3/10000/1984 pulled from it:
- Delete the --max-users cap entirely.
- Crawl round by round until the pending set (discovered minus done) is empty.
A user is "done" once we download its contact list, or after --max-attempts
(default 3) failed tries of its outbox — so an unreachable outbox can't stall
the crawl, and it still terminates on a finite graph.
- --max-rounds replaces --max-depth as an (unbounded by default) safety backstop.
- Track and report the pool of relays actually contacted (relays_contacted),
the "running relays" we connect to as more outboxes are discovered.
JSON: `depth_reached` -> `crawl_rounds`, add `relays_contacted`. Per-round and
final crawl-summary progress on stderr. Local regression: scores unchanged
(rank 26); the crawl retries contact-list-less users then terminates cleanly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
Correct the injector's relay model: indexer relays (purplepag.es, coracle, …)
aggregate kind:10002 (and kind:0) for the whole network but do NOT serve
kind:3/10000/1984. Those live only on each user's own outbox.
- Split the relay sets: `relayListDiscoveryRelays` (bootstrap + event-finder +
indexers) is used only to locate kind:10002; `contentFallbackRelays`
(bootstrap + event-finder, no indexers) is the best-effort fallback for
content when a user's outbox is unknown/down.
- Content is fetched from each user's outbox write relays, with harvested
relay hints and general relays as fallback — never indexers.
Also add progress status (all on stderr, stdout stays the JSON contract):
- loading already logs per-hop frontier/recovered/new/total counts;
- "graph built: N users, E edges; scoring…" and "scored N users" bracket the
calculation;
- GrapeRank.compute gains an optional (visited, scored, queued) progress
callback, wired to emit a scoring line every 5000 worklist visits so a large
graph shows movement instead of hanging silently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
To match Brainstorm's full-graph ingest, the crawl now discovers data through
three tiers (mirroring the app's pickRelaysToLoadUsers) instead of just the
outbox + a small fallback:
- Indexer relays (purplepag.es, coracle, …) join the discovery set. They serve
kind:0/3/10002 for the whole network and are where a stranger's relay list
and contact list are actually found — the biggest completeness lever.
- Per-follow relay hints are harvested from the `p`-tag hints in every contact
list we crawl and used as a discovery tier below each user's kind:10002.
- A per-hop retry pass re-queries any frontier member whose contact list still
didn't arrive (no kind:10002, or its outbox was unreachable) against the
indexer + hint set, recovering users the outbox model alone would miss.
This tightens the only real source of score divergence from Brainstorm — data
completeness — since a signal's weight scales by the rater's influence, so the
users that matter are exactly the in-graph ones this crawl now reaches more
reliably. Local regression check: scores unchanged (rank 26 for direct follows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
Analysis of NosFabrica/brainstorm_graperank_algorithm (Java scoring worker)
and NosFabrica/brainstorm_server (Python orchestration) to confirm amy's
scores match the reference GrapeRank service.
Finding: our commons/wot formula and every scoring parameter are already
identical to Brainstorm's DEFAULT preset (attenuation 0.85, rigor 0.5,
follow 1.0/0.03, from-observer 0.5, mute/report -0.1/0.5, delta 0.0001). Our
`score` is exactly their ScoreCard `influence`. Remaining divergence is data
completeness, not math — and because a signal's weight scales by the rater's
influence, only in-graph raters move a score, which our outbox crawl already
captures. Documents the pipeline, side-by-side params, divergence sources,
and follow-ups (presets, influence/verified fields).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB
Audited all 143 plan files across the 10 plans/ folders. Each plan now
carries a Status header (shipped | in-progress | queued | abandoned)
backed by codebase evidence, and every folder has a README.md index
grouping plans by status.
Shipped plans were moved into a per-folder plans/archive/ (via git mv,
history preserved) so each plans/ folder surfaces only live work:
shipped (archived): 122 in-progress: 8 queued: 7 abandoned: 4
docs/plans/ is the frozen legacy folder; its plans were stamped and
indexed in place (48 of 52 archived) but it remains closed to new plans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
Add the offline Cashu command tier to amy, all driven by the shared
commons wallet code so amy exercises the same path as the Android app:
amy cashu wallet create [--mint URL] [--mints a,b] [--privkey HEX] [--relay r1,r2]
amy cashu wallet show
amy cashu wallet export-key
amy cashu wallet destroy
amy cashu mint ping URL (stateless)
amy cashu mint info URL (stateless)
amy cashu balance [--mint URL]
- create/destroy reuse commons CashuWalletOps.publishWalletEvents /
deleteWallet; show/balance reuse the CashuWalletReader projection over
the local event store; mint ping/info hit quartz's MintHttpClient.
- Context gains cashuOps() (wired to the file NUT-13 counter store + a
per-run seed cache) and cashuSnapshot(); DataDir gains cashu.json.
- Extraction D: CashuKeysetCounterStore contract in commons +
FileCashuKeysetCounterStore (atomic ~/.amy/<account>/cashu.json).
PRs 4 of cli/plans/2026-05-28-cashu-cli.md (extractions A–D + offline
tier). receive/send/maintenance/mint-rec + interop harness still pending.
Verified end-to-end against mint.minibits.cash and live relays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
The framing: Amy gains every cashu action the Amethyst UI exposes, each
one reusing the exact same quartz + commons code the Android wallet
runs in production. The deliverable is a shell harness under
cli/tests/cashu/ that walks two Amy accounts through the full wallet
lifecycle against a production mint, so regressions in the cashu code
path fail on the JVM in CI without an emulator.
Plan covers:
- Three extractions before any verb lands: cashu token parsers to
quartz, CashuWalletOps to commons, CashuWalletReader projection
helpers to commons.
- One storage addition: ~/.amy/<account>/cashu.json for NUT-13 keyset
counters (deterministic secrets need durable counter state across
invocations).
- Command surface under amy cashu …: wallet, mint, balance, receive,
send, mint-rec, maintenance — mirrors every action the Android
wallet exposes.
- Stable --json shape per verb.
- 9-PR sequencing: extractions first, then verbs grouped by user
intent, then the 10-scenario interop harness.
- Acceptance criteria: harness passes against mint.minibits.cash and
the on-relay events are byte-equivalent to what Amethyst would
produce.
Three changes that go together:
1. Reconcile cli/plans/2026-04-24-file-event-store-{overview,nips}.md
with shipped reality: code lives in quartz/jvmMain/, not commons/;
data dir is <data-dir>/events-store/, not <root>/events/.
2. New StoreCommands wired as `amy store …`:
- stat → events count, kind histogram, disk bytes,
oldest/newest createdAt. Pure read, no Context
(skips identity check).
- sweep-expired → wraps store.deleteExpiredEvents(); reports
{swept, remaining}.
- scrub → wraps store.scrub() to rebuild idx/ from
canonicals.
- compact → wraps store.compact() to drop dangling idx/.
All four open the FsEventStore directly (no Context, no identity
needed) — they're store-only operations.
3. New e2e harness at cli/tests/cache/cache-headless.sh that boots a
local nostr-rs-relay, two amy identities (A + B), and asserts:
T1 — store stat reports non-empty store after publish-lists +
profile edit, with kind:0 and kind:10002 present.
T2 — A's `profile show` is `source: "cache"` by default.
T3 — `--refresh` forces `source: "relays"`.
T4 — B's first `profile show <A_NPUB>` is a relay miss; second is
a cache hit (proves drain populates the local store and
subsequent reads serve from disk).
T5 — `relay list` reads URLs back from the local kind:10002 /
10050 / 10051 events.
T6 — relays.json no longer exists in either data-dir.
T7 — store stat / sweep-expired / scrub / compact all run
without an identity present.
Same pattern as cli/tests/dm/dm-interop-headless.sh — reuses the
nostr-rs-relay infrastructure from cli/tests/marmot/setup.sh.
Three-part design doc for an IEventStore backed by a directory tree
instead of SQLite, targeted at the cli/ module.
- overview: goals, directory layout, feature-parity matrix, public API
- pipelines: insert (T1-T8 with crash-safety), query planner, delete,
transactions, concurrency
- nips: replaceable/addressable slot enforcement via hardlinks +
atomic rename, NIP-09 tombstones (hardlink to the kind-5), NIP-40
expirations, NIP-50 FTS via inverted-index hardlinks, NIP-62 vanish
cascade, tag indexing, seed file, scrub/compact, test strategy,
10-step rollout
Design doc for `amy dm send|list|await`, reusing the existing Quartz
gift-wrap (NIP-17/NIP-59/NIP-44) pipeline. Single file under
cli/plans/; extracts FilterGiftWrapsToPubkey from amethyst/ to commons/
as a prerequisite commit so the CLI can depend on it without pulling
in :amethyst.