- Route agent typing indicators (kind 20002) through the serverless pool
subscription so the 'agent is typing…' signal shows in serverless mode
(previously dropped — CHANNEL_EVENT_KINDS excluded 20002). useChannelTyping
now listens on serverless-event:<channel> when serverless; appendMessage
skips ephemeral typing events so they never enter the timeline.
- Retry serverless history fetch with short backoff on cold start: public
relays return 0/partial while pooled connections warm up, causing the
'blank for a minute after launch' feeling. Retry until events arrive.
- Agent reply retries the full relay fan-out with backoff when every relay
rate-limited (transient), instead of silently dropping the reply. Hard
rejections are not retried.
- Prune relays that fail the WebSocket handshake (403/paid/unreachable) from
the agent's working set so discovery and publish only use healthy relays.
- Launch managed agents with the CURRENT workspace relay list (serverless)
rather than the list frozen into the agent record at creation time, so
stale/paid relays drop out without recreating the agent.
- Auto-fill the recommended public relays when the serverless toggle is
enabled in the Add Workspace dialog (no more tapping each chip).
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.
useWorkspaceInit runs above the QueryClientProvider, so useQueryClient()
threw on mount and the window never showed. Cross-workspace cache isolation
is already handled by WorkspaceQueryProvider being keyed on workspaceKey
(fresh QueryClient per workspace via remount).
Channel lists, message timelines, members, and agent lists are keyed in
React Query without the workspace id, so switching workspaces bled the
previous workspace's channels/data into the new one. resetWorkspaceState()
now calls queryClient.clear() alongside the existing module-singleton
resets, per the AGENTS.md workspace-switch contract.
The serverless transport was behaving like it talked to the bespoke Sprout
server instead of a standard multi-relay Nostr client. Three real bugs:
1. Split-brain: messages were sent over the multi-relay pool (landing on
whichever relay accepts, e.g. nos.lol when damus rate-limits) but READ over
a single-relay live WS (damus only) — so your own message was invisible.
Fix: in serverless, send + history + LIVE SUBSCRIPTION all go through the
Rust multi-relay pool. New persistent pool subscribe()/unsubscribe() keeps a
REQ open on ALL relays at once and merges+dedups (standard Nostr, like damus
SimplePool). New commands: query_channel_messages, subscribe/unsubscribe_
channel_messages; events streamed to the UI via serverless-event:<channel>.
2. Query flood: get_channels ran {kinds:[39000],limit:5000} (discover ALL
network channels) + a 500-id member-count batch on a public relay. Fix:
serverless skips network-wide discovery; only your member channels are
listed (4 scoped queries).
3. Diagnostics: added [serverless]/[pool] backend logging and [relay] webview
console logging so failures are visible, not silent.
Proven with live tests against damus+nos.lol: multi-relay publish survives
per-relay rate-limit, and the live subscription delivers a message published
to a different relay than the naive primary (the split-brain fix).
Serverless private channels and DMs are made private by encryption (no
server to enforce access). Messages are NIP-17 gift-wrapped (kind 1059)
to every member; the relay only sees opaque blobs addressed by #p.
- crate::encrypted: build_gift_wraps (one per member) + unwrap_gift
- send_channel_message: routes serverless DM/private channels through
encryption (rumor=kind9 with h tag -> seal -> wrap per member)
- decrypt_gift_wrap command + relayClient encrypted fetch/subscribe
(query kind1059 #p=me, decrypt, route by inner h tag)
- proven end-to-end over wss://relay.damus.io (group A->{A,B,C}, B reads)
Agent-side gift-wrap support (sprout-acp) follows separately.
Serverless workspaces now take a comma-separated relay list (seeded with the
5 default public relays) for redundancy:
- ws_relay query/submit/publish fan out to all relays concurrently; reads
merge+dedup by event id, writes succeed if any relay accepts
- relay_ws_urls_with_override parses the comma list; live WS uses first relay
- setup UIs (Welcome + AddWorkspace) have multi-select relay chips
- integration test proves publish-to-2 / read-deduped-from-2
Add DEFAULT_PUBLIC_RELAYS (sourced from deez/crates/mesh-client
DEFAULT_RELAYS) as quick-pick chips when creating a serverless
workspace, in both the Add Workspace dialog and the Welcome screen.
- relay.damus.io, nos.lol, relay.nostr.band, nostr.land, nostr.wine
- chips one-tap fill the relay URL field (serverless mode only)
- DEFAULT_SERVERLESS_RELAY prefills when serverless is first enabled
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.