The agent connected to a single relay, so a message stored only on relay B
(because relay A rate-limited the write) was invisible to it — the split-brain.
Relays don't gossip (verified against damus RelayPool.swift, NIP-01/65/17,
nostr-tools SimplePool): clients must publish-to-many AND subscribe-to-many +
dedup by event id. The harness now spawns one background task per relay sharing
one event stream, fans out commands to all, and dedups in next_event() via a
bounded seen-id ring (like damus seenEvents).
Proven live: agent on [damus,nos.lol] receives a kind-9 published ONLY to
nos.lol (agent_multi_relay_subscribe_no_split_brain), plus the existing
encrypted-receive test now runs against a comma relay list.
The sprout-acp harness is single-relay but serverless workspaces pass a
comma-separated relay list, causing a DNS crash on the raw string. Split to the
first relay (matching the desktop live-WS convention).
Also: a generic public relay never emits the kind:44100 'member added'
notification the agent relies on to learn it was added to a new channel (that's
a Sprout-relay side effect). In serverless mode, periodically re-run
discover_channels (kind:39002 #p=agent) and subscribe to newly-joined channels.
Without this, a channel created/joined after the agent started is never watched.
Live test now passes a comma-separated relay list through connect_with_mode.
The ACP harness is single-relay but serverless workspaces pass a
comma-separated relay LIST (wss://a,wss://b,...). connect_with_mode fed
the raw comma string to connect_async, which failed DNS lookup
("nodename nor servname provided") and crashed the agent before it ever
subscribed — so it never saw mentions and never responded.
Split to the first (primary) relay, matching the desktop live-WS
convention. Regression-covered by the live agent test now using a
comma-separated RELAY_URL.
Add a live, #[ignore]d end-to-end test proving a HarnessRelay in serverless
mode (as a managed agent runs) receives + decrypts a NIP-17 gift-wrapped
message over a real public relay and surfaces it via next_event() with the
verified sender preserved. Matches the repo's e2e convention (RELAY_URL env,
default damus). Also wire RELAY_URL into the existing serverless live tests so
they aren't hardcoded to damus.
- Delete orphaned HarnessRelay::connect() wrapper (callers use
connect_with_mode); fold its doc into connect_with_mode.
- Gate BgState::new() behind #[cfg(test)] (test-only now that the
runtime path uses with_serverless) instead of #[allow(dead_code)].
- Move KIND_GIFT_WRAP into the encrypted test module (test-only).
No #[allow(dead_code)] / cfg_attr dead-code suppressions remain on
branch-new code. Verified with RUSTFLAGS='-D dead_code -D unused'.
The agent harness now subscribes to its own gift-wrap inbox (kind 1059,
#p = agent) alongside membership notifications, unwraps each, recovers
the inner kind-9 rumor (verified sender + h tag), and forwards it as a
normal SproutEvent. The respond-to allowlist gate and rule matching run
unchanged — so an agent in an encrypted private channel only responds to
blessed npubs, exactly as on the Sprout server.
- send_gift_wrap_subscribe (mirrors observer-control), tied to membership
sub (active + reconnect restore)
- dispatch: 1059 on GIFT_WRAP_SUB_ID -> UnwrappedGift -> rumor_to_event
- rumor_to_event: UnsignedEvent -> Event (seal already verified sender;
pipeline never reads .sig)
- nip59 enabled workspace-wide; unit test covers unwrap->channel event
The 'join to participate, nothing happens' bug: when a user creates or
joins a serverless channel they p-tag themselves (the signer). nostr 0.44
strips self-referencing p tags at sign time unless .allow_self_tagging()
is set, so kind:39002 membership events were published with an EMPTY
member list. get_channels then queries 39002 by #p:[me], finds nothing,
and the UI stays stuck on 'join to participate' forever.
(This worked in the old TS/slackest codebase because nostr-tools does not
perform that scrub.)
Fix: add .allow_self_tagging() to the serverless 39000 + 39002 builders
in both desktop events.rs and the shared sprout-sdk builders (CLI/agents).
Reproduced and verified end-to-end against wss://relay.damus.io via a new
#[ignore] integration test (serverless_create_join_roundtrip): create ->
join -> both members visible. Added no-network unit regression guards
(serverless_{members,metadata}_keeps_self_p_tag) in both crates.
clippy clean (desktop+sdk+cli); desktop + sdk tests pass; file-size gate ok.
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.