The serverless agent never actually posted a reply: three real bugs,
each found by an end-to-end test against live public relays (no LLM).
1. CLI replies hit a single relay. `sprout messages send` used only the
first relay, so when that relay rate-limited the agent ("noting too
much") the reply was dropped. SproutClient now fans out reads/writes
across the full relay list — publish succeeds if ANY relay accepts,
queries merge+dedup. lib.rs passes the whole ws list, not just the first.
2. Agent harness died on a single relay 503. RestClient (channel discovery,
sibling checks) queried only the primary relay; a damus 503 crashed
discovery with "channel discovery error". RestClient now holds the full
relay list and fans out the same way — one relay down no longer kills the
agent. HarnessRelay carries relay_urls; rest_client() passes them through.
3. CLI panicked on wss:// (no rustls CryptoProvider). The serverless WS
transport (tokio-tungstenite) builds TLS directly and, unlike reqwest,
does not auto-install a provider. Install aws_lc_rs at CLI startup.
This is the standard Nostr client model (damus RelayPool, nostr-tools
SimplePool): relays don't gossip, so a client publishes-to-many,
reads-from-many, and dedups. Server mode is unchanged (single URL, HTTP bridge).
New e2e test (crates/sprout-acp/tests/e2e_agent_responds.rs + stub_agent.sh):
creates a channel + adds the agent, spawns the real sprout-acp harness with a
no-LLM ACP stub, posts an @mention, and asserts the agent's reply lands on the
relay. Passes in ~6s even while damus is 503-ing the whole time.
Also: fix pre-existing channels-subcommand stability test drift (PR #712 added
`channels search`); remove paid/auth relays already done; drop dead helpers.
Adds a 'serverless' workspace mode that points the desktop app at any generic
public Nostr relay (e.g. wss://relay.damus.io) with zero Sprout server
infrastructure — no Postgres, no HTTP /query|/events bridge, no NIP-98 auth.
Reuses Sprout's native event kinds (39000 channel metadata, 39002 membership,
kind 9 messages, h-tag scoping); 'serverless' is purely a transport + auth
concern, so the server-mode path is untouched.
Transport: reads/writes go over plain WebSocket (REQ/EOSE, EVENT/OK) instead
of the HTTP bridge; NIP-42 AUTH is answered only if the relay challenges.
Desktop backend:
- AppState.serverless flag + is_serverless(); apply_workspace gains a flag
- ws_relay.rs: query_relay_ws / submit_event_ws / publish_signed_event_ws
- relay.rs: query_relay/submit_event + agent profile sync branch to WS
- events.rs: build_channel_metadata_serverless (39000) + members (39002)
- create_channel/open_dm publish those events directly (DM ids = UUIDv5 over
sorted participants so both sides converge without a server)
- managed agents inherit SPROUT_SERVERLESS into the ACP subprocess env
Agents (full support, not a follow-up):
- sprout-acp: RestClient + HarnessRelay gain serverless mode; query/submit use
plain WS, NIP-42 handshake skipped on (re)connect; SPROUT_SERVERLESS env/arg
- sprout-cli: SproutClient serverless WS query/submit; --serverless flag,
to_ws_url helper, NetworkMsg error variant
- npub respond-to permissions are in-process and work unchanged
Frontend:
- Workspace.mode + workspaceMode()/isServerlessWorkspace() helpers
- relayClient.setServerless() skips the AUTH-wait on connect; late challenges
still answered so writes succeed on relays that require auth
- ServerlessContext/useIsServerless() hides search, pulse, projects, workflows
- Serverless toggle in the Add Workspace dialog and Welcome screen
Docs: docs/SPROUT_LITE_MODE.md updated with the implementation + testing steps.