Skip to content

feat(trade): agent UX — lock-free discover, serve-handled takes, unresponsive warning, readable status, fee dry-run (stacked on #1119) - #1125

Merged
maxie-agent merged 7 commits into
feat/credit-tradefrom
feat/credit-trade-ux
Oct 11, 2026
Merged

maxie-agent merged 7 commits into
feat/credit-tradefrom
feat/credit-trade-ux

Conversation

@maxie-agent

Copy link
Copy Markdown
Collaborator

Stacked on #1119 (base feat/credit-trade-receive). Agent-UX changes for the standalone maxplayer-trade CLI, scope approved by Bob. No protocol, lock-duration, deadline or split-solver changes; no send.

Feedback each item answers

  1. "discover blocks / fails while serve or take runs" → discover is lock-free and read-only: ephemeral relay key, never opens the journal, never takes owner.lock, never creates a home.
  2. "I have to stop serve to take/list/cancel" → while serve owns the home, list/take/cancel hand the request to serve over <home>/serve.sock; serve runs it with exactly the client's bounds and stays the single writer. Same stdout, stderr and exit codes as a direct run.
  3. "a take just hangs with no idea why" → counterparty_unresponsive display state after 180 s without a counterparty message, shown in take, the serve log and status, with elapsed time and when the refund becomes available. Display only.
  4. "status is unreadable raw JSON" → status prints a human summary (unresolved first, exit-code legend). status --json gives byte-for-byte today's output (golden test).
  5. "I can't show the human fees before committing" → --dry-run on list/take shows both legs' fees, gross amounts, max total debit and whether the real command would refuse, with zero side effects. discover adds per-lot expected_fees (unknown when a mint is unreachable; never blocks).

Design

  • src/serve.rs: a Unix socket created while serve holds owner.lock, so any existing socket file is stale and gets replaced (a non-socket is refused). Mode 0600 inside the 0700 home; SO_PEERCRED same-uid check on both sides. One request per connection; requests capped at 16 KiB, reply lines at 64 KiB; 10 s I/O timeouts; at most 8 connections; deny_unknown_fields and no serde defaults for bounds, so serve can't fill in a missing or looser bound. The request carries the client's relay set and serve refuses a mismatch. Jobs go into coordinator::run_with and execute in serve's own select loop between inbox messages and recovery ticks, through the same coordinator functions (journal before effect), so a request is either journaled or rejected and never half-applied. A client that disconnects never cancels a request; serve keeps driving the take. If serve goes silent the client exits 3 and points to status.
  • Client order in main.rs: try the socket → try the lock → retry the socket for 5 s (covers serve still binding, or a short-lived writer such as recover) → "home is already in use". It never runs as a second writer.
  • src/observe.rs: read-only SQLite views (SQLITE_OPEN_READ_ONLY; nothing is created). Unresponsive state comes from the swap row's updated_time and role/state; refund time mirrors the coordinator gates without deciding anything. Lab-only TRADE_LAB_UNRESPONSIVE_SECONDS exists for tests.
  • src/dryrun.rs: read-only keysets, plus a read-only wallet proof selection that mirrors mint::plan.
  • src/report.rs: the human status.
  • Bug found and fixed by the race test (0194bb5): the lot id hashes second-granular terms, so two identical lists in the same second (easy once serve queues them back to back) shared an id, and the second journal row overwrote the first and orphaned its reservation. list now waits for a fresh id or refuses. Two identical CLI lists in the same second could hit this before too, but the home lock made it unlikely.

Test map (tests/ux.rs, plus unit tests in observe/serve/dryrun)

Safety requirement Test
No two writers under concurrent clients + serve race concurrent_clients_and_serve_never_make_two_writers (lab writer-tenure trace, success count = journaled listings, no input reserved twice); live_serve_owns_every_write
Serve-handled take never uses a looser bound take_via_serve_enforces_client_bounds_exactly_and_settles; unit request_rejects_unknown_fields_and_keeps_bounds
Dry-run leaves journal, reservations, wallet DB and relay unchanged dry_run_has_zero_side_effects_and_matches_real_plan, dry_run_refusals_and_unsolvable_amounts_are_reported
Discover during a mid-swap serve leaves the journal unchanged discover_is_lock_free_and_never_touches_the_journal, discover_marks_unreachable_mint_fees_unknown_without_blocking
status --json golden status_json_matches_golden_and_summary_is_readable_and_secret_free
Stale socket / bounded protocol / 0600 stale_socket_and_bounded_protocol, unit bounded_lines
Unresponsive is display-only counterparty_unresponsive_warning_is_display_only, observe unit tests
Client disconnect disconnected_client_leaves_take_with_serve

Verification (local, head before this PR's merge commit fast-forward: 81e6ea6)

  • cargo fmt --check; clippy default + lab with -D warnings and the CI allow list; all --locked.
  • Default: 132 passed (lib 34, cli 5, e2e 6, money 16, pinned 1, receive 18, relays 7, review_regressions 17, round5 20, ux 8). Lab (TRADE_LAB_SECONDS=1): 182 passed (lib 34, cli 5, e2e 11, money 17, pinned 1, receive 23, relays 7, review_regressions 46, round5 26, round7 1, ux 11).
  • The race test passed 15/15 consecutive lab runs after the lot-id fix (it failed 3 of 5 before).
  • Mutation check of the safety tests in a separate target dir. All 9 mutants were killed; each unmutated baseline passed:
    M1 discover takes the lock path · M2 serve take uses default max_fees · M3 serve loosens max_give +1 · M4 take --dry-run falls through to the real take · M8 list --dry-run falls through · M5 status --json gains a field · M7 socket 0666 · M6 owner-lock failure ignored · M9 lot-id collision guard removed.

Merge of #1119

Merged origin/feat/credit-trade-receive at 25cce15 (typed MintRefusal, persist-before-report, receive status summary). The SKILL.md conflict was resolved so both sets of changes survive; it is 9,996 B, under the 10,000 B test limit. The golden test and #1119's receive status assertion now use status --json.

Unverified

  • Only Linux was tested. Long homes reach the socket via /proc/self/fd/N; the SO_PEERCRED behavior is Linux-specific.
  • No real mints or relays: loopback lab mints and relay only.
  • A cross-uid refusal was not exercised; tests run as a single user. The uid check is code-reviewed only.
  • Dry-run fees can differ from the real run if the mint's keysets or the wallet change between the two runs. Dry-run is a preview, not a reservation.

…sponsive warning, readable status, fee dry-run

- discover: ephemeral relay key, no owner.lock/journal/home creation; per-lot expected fees (unknown when a mint is unreachable)
- serve binds <home>/serve.sock (0600, same-uid peers, one bounded request per connection); list/take/cancel hand off with the client's exact bounds and run inside serve's loop (single writer)
- counterparty_unresponsive display state after 180 s (take, serve log, status); display only
- status prints a human summary; status --json unchanged (golden test)
- --dry-run on list/take: both legs' fees, gross and max debit, zero side effects
- tests/ux.rs safety tests
The lot id hashes second-granular terms; two identical list requests handed to serve
back-to-back produced the same id and the second journal row overwrote the first,
orphaning its reservation. list now waits for a fresh id (or refuses). Found by the
concurrent clients/serve race test, which now prints client and serve output on failure.
…tRefusal, persist-before-report, receive status; keep both docs sets)
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
maxplayer Ready Ready Preview Oct 11, 2026 2:06am UTC

Request Review

@maxie-agent
maxie-agent changed the base branch from feat/credit-trade-receive to feat/credit-trade October 11, 2026 01:13
…dit-trade-ux

# Conflicts:
#	crates/maxplayer-trade/skills/maxplayer-trade/SKILL.md
#	crates/maxplayer-trade/skills/maxplayer-trade/references/commands.md
#	crates/maxplayer-trade/src/coordinator.rs
#	crates/maxplayer-trade/src/lib.rs
#	crates/maxplayer-trade/src/main.rs
@maxie-agent
maxie-agent marked this pull request as ready for review October 11, 2026 02:52
@maxie-agent
maxie-agent merged commit c7f3077 into feat/credit-trade Oct 11, 2026
11 checks passed
maxie-agent added a commit that referenced this pull request Oct 11, 2026
Approved by Bob in Discord #cashu-token-marketplace (2026-10-10 20:38 UTC, stack merge go). Verified: head c7f3077 (contains #1119, #1121, #1123, #1125), CI 11/11 green, CLEAN.

This branch was successfully deployed

1 active deployment
Preview — 6f58c73c Deployed Oct 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant