Commit Graph
690 Commits
Author SHA1 Message Date
Michael Neale 51c89d4e64 fix(serverless): standard multi-relay Nostr — fix split-brain reads, query flood, single-relay live subscription
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).
2026-06-02 15:35:28 +10:00
Michael Neale bf6be68d79 fix(serverless): persistent connection pool — stop rate-limit storm
Root cause of 'everything is rate-limited' / 'noting too much': the
serverless transport opened a FRESH WebSocket per query and per publish
(connect→send→close). get_channels fires ~10 queries, create channel 2
publishes, add-agent several more — each a new connection. Public relays
(damus, nos.lol) aggressively rate-limit connection storms + event
bursts, so every action failed.

Fix (validated against damus/nostr-tools idiomatic patterns + NIP-01/65):
- ws_pool.rs: one persistent WebSocket per relay, reused for all
  queries/publishes; background reader dispatches by sub/event id;
  transparent reconnect; NIP-42 AUTH answered on the same socket.
- Don't block on slow relays: publish returns on FIRST acceptance
  (FuturesUnordered); connect has a 4s timeout; failed relays enter a
  30s cooldown so a dead relay isn't re-dialed every query.
- Multi-relay publish (success if any accepts) confirmed idiomatic —
  not a mistake; it's the resilience model and hides per-relay rate caps.
- Pool cleared on workspace switch.

Verified live: 5-relay burst (15 queries + 6 publishes) that previously
hung 3+min now completes in 17s with zero rate-limits; create→join
roundtrip passes on 2 relays. 426 unit tests pass, clippy clean.

New live test: serverless_burst_no_rate_limit.
2026-06-02 15:05:25 +10:00
Michael Neale 8e8f93a85b fix(serverless): local-only agents, rate-limit retry, channel delete
- list_relay_agents returns empty in serverless: managed agents are local
  desktop subprocesses, not a relay registry. Fixes 'agent parse failed:
  missing field name' from foreign kind-10100 events on public relays.
- submit_event_ws retries once on all-relays-rate-limited (public relays
  throttle the write burst when adding an agent). Succeeds if any relay
  accepts. Fixes 'Failed to add goose: rate-limited'.
- delete_channel publishes NIP-09 (kind 5) deletion over the 39000/39002
  addressable coordinates in serverless mode (the kind-9008 command is a
  no-op on a generic relay). Owner-only.
- Tests + file-size exception.
2026-06-02 14:37:02 +10:00
Michael Neale 6b756dae1e fix(serverless): record member roles in kind:39002 (creator=owner)
The serverless membership event only stored bare ['p', pubkey] tags with
no role, so every member — including the channel creator — read back as
'member'. This hid the member-invite card on private channels (it requires
owner/admin) and diverged from server mode, where the relay assigns the
creator 'owner'.

Store roles as the 4th p-tag element (['p', pubkey, '', role]) — the NIP-29
convention channel_members_from_event already reads. Creator = owner; new
members default to 'member'; role changes remove+re-add; existing members
keep their role across read-modify-write. Matches server-mode data shape.
2026-06-02 14:23:57 +10:00
Michael Neale 6f5f6df71c fix(serverless): build ChannelInfo locally on create/open_dm (fixes 'nothing happened')
The serverless create_channel and open_dm re-queried the relay for the
kind:39000 they had just published, to build ChannelInfo. On a public relay
that immediate round-trip races against propagation/indexing lag and can
return empty, throwing 'metadata not yet available' — surfacing as a private
channel / DM that silently fails to appear in the UI.

Both now derive ChannelInfo directly from the locally-signed metadata event
(we created it, so is_member = true), with no relay round-trip and no race.
2026-06-02 13:37:12 +10:00
Michael Neale a568920cf9 test(serverless): live agent encrypted-receive test + RELAY_URL override
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.
2026-06-02 12:47:00 +10:00
Michael Neale ba335aed2a refactor(serverless): remove dead code introduced on branch
- 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'.
2026-06-02 11:32:39 +10:00
Michael Neale 191de54143 docs(serverless): document multi-relay, agents, and NIP-17 encryption 2026-06-02 10:33:00 +10:00
Michael Neale b93eb61570 feat(serverless): agents receive NIP-17 encrypted messages (sprout-acp)
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
2026-06-02 10:15:56 +10:00
Michael Neale 74deb9cf77 feat(serverless): NIP-17 encrypted DMs + private channels (desktop)
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.
2026-06-02 10:10:10 +10:00
Michael Neale 8934a07051 feat(serverless): multi-relay support — publish-to-all, read-deduped
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
2026-06-02 09:25:24 +10:00
Michael Neale 858cb3b02a fix(serverless): allow self-tagging on channel membership events
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.
2026-06-02 08:43:09 +10:00
Michael Neale fc832f4d5d fix(serverless): membership via direct kind:39002 (join/leave/add/remove) + CLI channel ops
In serverless mode there's no relay to process membership command kinds
(9021 join, 9022 leave, 9000/9001 add/remove member). The client now mutates
the replaceable kind:39002 member-list event directly (read-modify-write).

- desktop: join/leave/add/remove_channel_member publish 39002 directly
- sprout-sdk: build_channel_metadata_serverless (39000) +
  build_channel_members_serverless (39002), shared builders
- sprout-cli: channels create/join/leave/add-member/remove-member serverless
  branches (fixes agent replies + CLI parity with desktop)

Verified end-to-end against wss://relay.damus.io: create -> list -> send ->
read all round-trip with zero Sprout infrastructure.
2026-06-02 08:33:06 +10:00
Michael Neale c508603543 feat(serverless): default public relays from deez mesh client
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
2026-06-02 08:05:57 +10:00
Michael Neale 84f84931f1 feat(serverless): run channels, DMs, messages, and agents against a generic Nostr relay
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.
2026-06-01 20:42:29 +10:00
bc23620fcf Fix desktop glass chrome and inbox previews (#793)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 20:27:16 -07:00
Will PflegerandGitHub 9b9cf46127 refactor(just): slim down mobile-dev to just run Flutter (#801) 2026-05-29 20:44:32 -04:00
Will PflegerandGitHub 2a03851527 refactor: consolidate infra management into justfile + add mobile-dev (#797) 2026-05-29 18:26:24 -04:00
WesandGitHub 995189c223 chore(release): release version 0.3.5 (#799) v0.3.5 2026-05-29 14:48:49 -07:00
b820420909 feat(mobile): Pulse polish — flat feed, compose page, shared filter chips, like + accent fixes (#796)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-05-29 14:33:45 -07:00
Will PflegerandGitHub 10f37e4cab feat(desktop): add standalone Playwright screenshot helper (#795) 2026-05-29 17:10:56 -04:00
Will PflegerandGitHub f34a21d32f feat(sprout-agent): load AGENTS.md and SKILL.md into system prompt (#762) 2026-05-29 14:44:33 -04:00
Will PflegerandGitHub 85861f33fe feat: add code block support to message composer (#788) 2026-05-29 11:38:10 -07:00
Will PflegerandGitHub 7beb0f8e68 fix(desktop): reap orphaned agent processes on shutdown and restart (#787) 2026-05-29 18:18:55 +00:00
WesandGitHub 8fc27b6554 chore(release): release version 0.3.4 (#786) v0.3.4 2026-05-29 09:50:08 -07:00
d77b111b15 Update desktop navigation chrome and search (#779)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 08:26:41 -07:00
tlongwell-blockandGitHub 5ee2cd0517 feat(desktop): reload webview on Cmd/Ctrl+R (#785) 2026-05-29 10:24:56 -04:00
Will PflegerandGitHub fa7febe40f fix(desktop): sync persona pack directory across worktree instances (#782) 2026-05-28 20:41:23 -04:00
WesandGitHub a28a0e6b42 chore(release): release version 0.3.3 (#781) v0.3.3 2026-05-28 16:51:47 -07:00
WesandGitHub c761a76ff2 fix(release): sync release tags during preflight (#780) 2026-05-28 16:29:25 -07:00
Will PflegerandGitHub 3f3ec64791 feat(desktop): thread-aware notifications with mutable follow/mute controls (#761) 2026-05-28 18:43:41 -04:00
Will PflegerandGitHub 03e678cfbc fix(desktop): improve model picker message and runtime dropdown clarity (#778) 2026-05-28 18:21:12 -04:00
WesandGitHub 9db8f6ccb8 desktop: float unread indicator + fix sidebar scroll jump (#777)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-28 15:15:19 -07:00
Will PflegerandGitHub 3fbee555f0 chore(hooks): standardize check/fix convention with auto-fix pre-commit (#776) 2026-05-28 17:42:35 -04:00
0f89ad1692 web: clickable repo tree + per-file blob viewer (#773)
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
2026-05-28 15:55:03 -04:00
tlongwell-blockandGitHub 61297ac80a fix(agent): keep parallel tool-result messages contiguous on OpenAI Chat (Databricks image fix) (#770)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-05-28 15:25:51 -04:00
Will PflegerandGitHub 5f2423c231 fix(release): fetch tags so changelog tracks versions correctly (#775) 2026-05-28 19:17:35 +00:00
WesandGitHub 8da9acbad9 chore(release): release version 0.3.2 (#774) v0.3.2 2026-05-28 12:00:40 -07:00
WesandGitHub 1218572fa0 feat(mobile): add Pulse social feed tab (#772)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-28 11:09:44 -07:00
WesandGitHub fec6a683ea feat(sidebar): add More unread floating buttons (#771)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-28 11:05:02 -07:00
Taylor HoandGitHub 8eedec7403 chore: improve markdown spacing (#766) 2026-05-28 17:35:10 +00:00
Taylor HoandGitHub 835a44aad0 fix: prevent inline links rendering in 2-column grid layout (#767) 2026-05-28 17:34:35 +00:00
WesandGitHub 9a10e6281e chore(release): release version 0.3.1 (#769) v0.3.1 2026-05-28 09:10:28 -07:00
WesandGitHub 4222a758c6 [codex] Default release command to patch bump (#768) 2026-05-28 08:48:12 -07:00
WesandGitHub 30654e95f6 Polish desktop Pulse and Home views (#764)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-28 08:17:02 -07:00
Will Pfleger 516dc8303f fix(release): guard CHANGELOG.md read and fix macOS head -n -1
The release notes extraction failed on two counts: awk errors when
CHANGELOG.md doesn't exist (first release has no changelog), and
head -n -1 is a GNU extension unsupported on macOS runners. Guard
with a file existence check and replace head with sed '$d'.
v0.3.0
2026-05-27 19:45:20 -04:00
Will PflegerandGitHub bcd903edcc feat(release): automate OSS release pipeline with just release (#757) 2026-05-27 19:25:09 -04:00
af8b0b4cfc fix(relay): install git in runtime image (#763)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
2026-05-27 18:12:04 -04:00
Will PflegerandGitHub f42839c740 feat(desktop): worktree agent data sync, retired persona cleanup, mcp_command reconciliation (#728)
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
v0.0.22
2026-05-27 16:46:16 -04:00
WesandGitHub 35821574c0 feat(desktop): redesign status UI with full emoji picker and presence sub-menu (#760)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-27 13:13:33 -07:00