Commit Graph

747 Commits

Author SHA1 Message Date
Michael Neale
ea77a0bcfe fix(merge): reconcile build_message custom_emoji_tags arg + dedup tauri allow attr
Post-merge fixes for desktop-tauri compile (clippy compiles tests):
- build_message gained custom_emoji_tags param from main; pass &[] in the
  three serverless test/helper call sites (channels_tests, encrypted)
- remove duplicate #[allow(too_many_arguments)] left by the merge
2026-06-04 15:18:46 +10:00
Michael Neale
4c49c8f521 Merge remote-tracking branch 'origin/main' into micn/serverless-mode
* origin/main: (36 commits)
  fix: use immutable commit-SHA URLs in screenshot PR comments (#842)
  feat(mobile+desktop): two-tier Slack-style app icon badge (#802)
  chore: simplify file-size check to a flat 1000-line limit (#839)
  fix(desktop): robust emoji picker — unify picker + fix custom emoji in editing, status, reactions (#837)
  feat(desktop): reusable screenshot workflow for agents (#826)
  desktop(mesh-llm): let a serving node route a different model (#833)
  chore(release): release version 0.3.9 (#832)
  fix: native arbitrary-file download + image context-menu flash (#830)
  fix(desktop): custom emoji reaction rendering + picker autofocus (#831)
  Mesh-LLM v1: relay-gated direct-iroh inference between users (WAN) (#822)
  chore(release): release version 0.3.8 (#829)
  chore(release): release version 0.3.7 (#825)
  feat: code block rendering, syntax highlighting, and compose fixes (#803)
  feat: custom emoji — user-owned NIP-30 sets with a client-side union (#816)
  Install sprout-cli skill at repo root + fix desktop clippy (#818)
  fix(desktop): use public re-export path for ensure_client_node_for_model (#824)
  refactor(desktop): feature-gate mesh-llm-sdk behind optional Cargo feature (#823)
  fix(desktop): align workflow read/save commands to the frontend contract (#820)
  fix(desktop): disable mesh-llm auto-build to prevent git config corruption (#819)
  fix(desktop): clear clippy lints in agents/mesh_llm commands (#817)
  ...

# Conflicts:
#	Cargo.lock
#	desktop/scripts/check-file-sizes.mjs
#	desktop/src-tauri/Cargo.toml
#	desktop/src/app/AppShell.tsx
#	desktop/src/app/AppTopChrome.tsx
#	desktop/src/features/messages/hooks.ts
#	desktop/src/features/workspaces/useWorkspaceInit.ts
#	desktop/src/shared/api/tauri.ts
2026-06-04 15:08:56 +10:00
Will Pfleger
2a0572c0d8 fix: use immutable commit-SHA URLs in screenshot PR comments (#842) 2026-06-03 22:28:11 -04:00
Will Pfleger
3b78dc5690 feat(mobile+desktop): two-tier Slack-style app icon badge (#802) 2026-06-03 18:16:49 -07:00
Michael Neale
fcb62472f1 fix(serverless): clippy cloned_ref_to_slice_refs in sdk tests 2026-06-04 09:56:49 +10:00
Michael Neale
a9b627c4b6 fix(serverless): log encrypted-recipient resolution on send (diagnostic) 2026-06-04 09:53:26 +10:00
Wes
0c225f4d7d chore: simplify file-size check to a flat 1000-line limit (#839)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-06-03 16:40:33 -07:00
Michael Neale
6c7dc2753e fix(serverless): encrypted send picks newest 39002 across relays, fails loudly if members unresolved
The desktop encrypted_recipients() lookup used .first() on a multi-relay
merge of the addressable kind:39002 membership event and silently fell
back to encrypting to only self when the lookup came back empty. With the
membership event present on only some relays (split-brain / propagation
lag), a private-channel message could be gift-wrapped to just the sender —
so the agent (and other members) never received it, with no visible error.

Now: pick the NEWEST 39002 by created_at, and if an encrypted channel
resolves to no other members, return an error so the send fails loudly and
can be retried, instead of silently dropping every other recipient.
2026-06-04 09:05:11 +10:00
Wes
d8b602a359 fix(desktop): robust emoji picker — unify picker + fix custom emoji in editing, status, reactions (#837)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-06-03 15:13:25 -07:00
Michael Neale
a4183e263c test(serverless): prove agent replies in DM (gift-wrapped, deterministic channel id)
Adds agent_responds_in_dm_e2e — completes the functional trio (public
channel, private channel, DM). A DM in serverless is a private channel
with a deterministic UUIDv5 id derived from the participants (matching
desktop/src-tauri/src/commands/dms.rs). The message is gift-wrapped to
the agent; the test asserts the agent decrypts it and replies.

All three pass against live public relays (3 passed; 0 failed).
2026-06-04 06:45:00 +10:00
Will Pfleger
06bc67fd34 feat(desktop): reusable screenshot workflow for agents (#826)
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
2026-06-03 19:33:56 +00:00
tlongwell-block
9f0c22a43a desktop(mesh-llm): let a serving node route a different model (#833)
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
2026-06-03 14:30:46 -04:00
Wes
b4f8f40333 chore(release): release version 0.3.9 (#832) v0.3.9 2026-06-03 10:45:22 -07:00
Wes
82ae85f79a fix: native arbitrary-file download + image context-menu flash (#830)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-06-03 09:59:57 -07:00
Wes
7797ae77f6 fix(desktop): custom emoji reaction rendering + picker autofocus (#831)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-06-03 08:53:43 -07:00
tlongwell-block
33cfc85293 Mesh-LLM v1: relay-gated direct-iroh inference between users (WAN) (#822)
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
2026-06-03 10:49:09 -04:00
Michael Neale
b601a92861 docs(serverless): add design report (architecture, decisions, verification) 2026-06-03 16:36:41 +10:00
Michael Neale
8846ef3c12 test(serverless): prove agent receives + replies in private (encrypted) channel
Adds an e2e test that sends a NIP-59 gift-wrap (kind 1059) to the agent —
exactly what the desktop publishes for an encrypted channel — and asserts the
agent decrypts it, extracts the channel from the inner rumor's h tag, passes the
gate, and replies. Proves the private-channel path end-to-end against live
relays; the earlier 'goose didn't reply in private' was the agent being offline,
not a code bug.
2026-06-03 16:13:39 +10:00
Michael Neale
9745e9940d docs(serverless): explain why desktop ws_pool is hand-rolled vs agent's nostr-relay-pool 2026-06-03 15:02:38 +10:00
Michael Neale
604f5f3b2e fix(serverless): encrypt threaded replies in private channels (close plaintext leak)
Replies in encrypted serverless channels (DM/private) were falling through
to the plaintext path — a privacy leak: the reply content went to public
relays as a cleartext kind-9. The encrypt branch was gated on
parent_event_id.is_none(), so only top-level messages were gift-wrapped.

Fix: encrypt replies too. The NIP-10 thread tags now live INSIDE the rumor
(the encrypted inner event), so threading is preserved without leaking the
reply. The thread root is resolved locally from decrypted messages (the
parent rumor isn't queryable in plaintext on the relay) and passed via a new
root_event_id command arg.

Proven by encrypted_threaded_reply_is_wrapped_and_preserves_thread: asserts
the wire event is kind-1059 (not plaintext kind-9), content never leaks, and
the decrypted rumor carries the NIP-10 root + reply e-tags.
2026-06-03 15:00:06 +10:00
Michael Neale
2d76bec1a0 test(serverless): prove agent reply + typing cue + cancel/redispatch e2e
Extend e2e_agent_responds to exercise the paths the live GUI hits:
- second @mention proves the OwnerInterrupt cancel/redispatch path (a
  follow-up message gets a fresh reply) — previously untested
- live listener asserts the typing indicator (kind 20002) fires during a
  turn so the 'agent is typing…' cue reaches relays in serverless
- stub now replies per-prompt (-1, -2) instead of once

Fixes found by the test:
- reaction working-cue timeout 500ms -> 4s: the pool does a real
  WebSocket publish + OK round-trip across public relays, too slow for
  500ms, so the 💬 cue never landed
- typing listener now watches ALL relays (agent publishes to whichever
  accepts; one relay may be 503-ing)
2026-06-03 11:47:24 +10:00
Wes
7c896d339d chore(release): release version 0.3.8 (#829) v0.3.8 2026-06-02 16:57:08 -07:00
Wes
dc6821667d chore(release): release version 0.3.7 (#825)
Signed-off-by: Mesh-LLM CI <ci@mesh-llm.local>
Co-authored-by: Mesh-LLM CI <ci@mesh-llm.local>
v0.3.7
2026-06-02 14:56:23 -07:00
Will Pfleger
56c504a4c7 feat: code block rendering, syntax highlighting, and compose fixes (#803) 2026-06-02 14:43:08 -07:00
Wes
2349421304 feat: custom emoji — user-owned NIP-30 sets with a client-side union (#816)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-06-02 14:19:07 -07:00
Michael Neale
cc9b0b22c4 refactor(serverless): back agent relay layer with nostr-relay-pool
The serverless agent path hand-rolled multi-relay subscribe/reconnect/
resubscribe/dedup in relay.rs — reimplementing what nostr-relay-pool
(rust-nostr) already does, with gaps that caused the agent to miss
messages when a single relay 502'd mid-session.

Replace the serverless backend with nostr-relay-pool's RelayPool, which
provides the standard Nostr client model used by damus/0xchat/nostr-tools:
- persistent REQ per relay, fanned out across all relays
- auto-reconnect (default) with jittered backoff
- auto-resubscribe on reconnect (relays replay stored events on REQ)
- merge + dedup across relays (Event notification = first-seen only)

HarnessRelay now dispatches to ServerlessRelay when serverless; server
mode (NIP-42 AUTH + HTTP bridge) is unchanged. RestClient and the typing/
presence publisher share the pool. Subscription IDs match the server-mode
scheme so event routing is identical.

Proven: e2e_agent_responds passes 3x consecutively (~3s each) against live
public relays, surviving damus 503s via nos.lol.

- 272 unit tests pass, clippy clean
2026-06-03 07:16:12 +10:00
Will Pfleger
7a12e50518 Install sprout-cli skill at repo root + fix desktop clippy (#818) 2026-06-02 17:11:30 -04:00
Will Pfleger
2ea3fd88d2 fix(desktop): use public re-export path for ensure_client_node_for_model (#824) 2026-06-02 16:59:05 -04:00
Michael Neale
e56c5f31c4 feat(serverless): typing indicators + cold-start history retry
- 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.
2026-06-03 06:48:40 +10:00
Michael Neale
12241a5fda fix(serverless): launch agents with current workspace relays + real dev sidecars
Agent restore at startup raced ahead of the frontend applying the active
workspace, so serverless agents launched with the stale relay list frozen
into their record at creation time (including paid relays that 403) and one
even ran in server mode. When the only healthy relay rate-limited a reply,
there was no failover and the reply silently vanished.

- AppState.workspace_applied: agent restore now waits (bounded 15s) for the
  frontend to call apply_workspace before launching, so agents inherit the
  CURRENT workspace relays + serverless mode instead of record defaults.
- apply_workspace sets the flag once config is live.
- justfile _ensure-sidecar-stubs builds REAL sprout-acp + sprout sidecars
  (serverless agents need them) instead of 0-byte stubs that silently fail
  to launch/reply under just dev.
2026-06-03 06:40:57 +10:00
Will Pfleger
fb514c8918 refactor(desktop): feature-gate mesh-llm-sdk behind optional Cargo feature (#823) 2026-06-02 20:25:01 +00:00
Wes
b72eee365f fix(desktop): align workflow read/save commands to the frontend contract (#820)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-06-02 12:52:56 -07:00
Will Pfleger
5b572d6f5e fix(desktop): disable mesh-llm auto-build to prevent git config corruption (#819) 2026-06-02 18:49:41 +00:00
Wes
192388a3cb fix(desktop): clear clippy lints in agents/mesh_llm commands (#817)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-06-02 11:01:01 -07:00
Wes
41a3fc1589 fix(desktop): let channel members add members and bots without admin (#815)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-06-02 09:55:09 -07:00
Wes
a25ca5d1bf Desktop #806 follow-ups: panel/inbox fixes + top-bar backdrop (#814)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-06-02 08:45:04 -07:00
thomaspblock
5bed17a117 Fix desktop right-side panel chrome overlap (#806)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 07:37:36 -07:00
tlongwell-block
ede8ddb425 Sprout × mesh-llm: in-process mesh node (serve/consume) + relay admission (#798) 2026-06-02 10:28:20 -04:00
Michael Neale
104588ff20 fix(serverless): make agent replies resilient to dead/rate-limited relays
- 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).
2026-06-02 19:12:28 +10:00
Michael Neale
1293881b72 fix(serverless): multi-relay fan-out for agent CLI + harness, prove agent replies e2e
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.
2026-06-02 18:02:12 +10:00
Michael Neale
884dfa59ad fix(serverless): revert useQueryClient in useWorkspaceInit (crashed window)
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).
2026-06-02 17:17:13 +10:00
Michael Neale
10b8f06463 fix(serverless): clear React Query cache on workspace switch
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.
2026-06-02 17:05:57 +10:00
Michael Neale
5afc0e0266 fix(serverless): agent subscribes to ALL relays + dedup (damus RelayPool pattern)
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.
2026-06-02 16:56:56 +10:00
Michael Neale
bc92b752e0 fix(serverless): agent re-discovers channels (no 44100 push) + split 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.
2026-06-02 16:39:07 +10:00
Michael Neale
0b679ccfb1 fix(serverless): agent connects to first relay of comma list (DNS crash)
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.
2026-06-02 15:43:31 +10:00
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