mirror of
https://github.com/block/buzz.git
synced 2026-11-01 06:51:17 +00:00
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.
This commit is contained in:
@@ -70,7 +70,7 @@ const overrides = new Map([
|
||||
["src/features/settings/ui/ChannelTemplatesSettingsCard.tsx", 850], // template CRUD card + TemplateFormDialog (persona/team chip selectors + provider assignments + canvas template) + TemplateTeamSelector + ProviderAssignments + ProviderRow
|
||||
["src/shared/api/types.ts", 650], // ... + AcpProviderCatalogEntry + AcpProvider (narrowed subtype) + InstallRuntimeResult + RespondToMode + respondTo/respondToAllowlist on ManagedAgent/Create/Update inputs
|
||||
["src-tauri/src/commands/channels.rs", 670], // serverless membership now carries roles (creator=owner) in kind:39002 p-tags + serverless membership read-modify-write of kind:39002 (join/leave/add/remove publish member list directly) + create_channel branches serverless (publish 39000+39002 directly) vs server (kind 9007 command) + channel CRUD commands // create_channel branches serverless (publish 39000+39002 directly) vs server (kind 9007 command) + channel CRUD commands
|
||||
["src-tauri/src/events.rs", 1070], // + role-aware serverless members builder (owner role) + tests // + self-p-tag regression tests (allow_self_tagging on 39000/39002) // serverless addressable builders (build_channel_metadata_serverless 39000 + build_channel_members_serverless 39002) + their tests + event builders + build_huddle_guidelines (kind:48106) + post_event_raw transport helper + participant p-tag on join/leave + NIP-43 relay admin builders (add/remove/change-role) + check_relay_role + DM/presence/workflow command builders + NIP-IA identity-archive builders (9035/9036) + .allow_self_tagging() guards (nostr 0.44 strips self-`p` by default; self-archive/unarchive needs it preserved) + spec vector 1 layout test
|
||||
["src-tauri/src/events.rs", 1110], // + serverless channel delete (NIP-09 kind 5 over 39000/39002 coords) + role-aware serverless members builder (owner role) + tests // + self-p-tag regression tests (allow_self_tagging on 39000/39002) // serverless addressable builders (build_channel_metadata_serverless 39000 + build_channel_members_serverless 39002) + their tests + event builders + build_huddle_guidelines (kind:48106) + post_event_raw transport helper + participant p-tag on join/leave + NIP-43 relay admin builders (add/remove/change-role) + check_relay_role + DM/presence/workflow command builders + NIP-IA identity-archive builders (9035/9036) + .allow_self_tagging() guards (nostr 0.44 strips self-`p` by default; self-archive/unarchive needs it preserved) + spec vector 1 layout test
|
||||
["src-tauri/src/huddle/mod.rs", 1020], // huddle state machine + Tauri commands + sync protocol doc; state/relay/pipeline extracted + emit_huddle_state_changed wiring
|
||||
["src-tauri/src/huddle/models.rs", 950], // model download manager for Parakeet TDT-CTC STT + Pocket TTS with streaming downloads + SHA-256 verification + Rust-native tar extraction + version manifest + atomic swap + hot-start signaling + MODEL_LICENSE.txt sidecar (fail-closed readiness) + idempotent legacy Moonshine dir cleanup + tts_readiness_requires_license_sidecar test + Mary (VCTK p333) reference voice attribution block
|
||||
["src-tauri/src/huddle/stt.rs", 580], // STT pipeline + PTT edge-detection flush + PTT gating (is_speech AND ptt_active) + barge-in for VAD mode + rubato resampler + earshot VAD + sherpa-onnx transcription
|
||||
|
||||
@@ -308,6 +308,16 @@ pub fn discover_managed_agent_prereqs(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_relay_agents(state: State<'_, AppState>) -> Result<Vec<RelayAgentInfo>, String> {
|
||||
// Relay-side agent discovery (kind:10100 registry) is a Sprout-server
|
||||
// concept. In serverless mode, managed agents are purely local desktop
|
||||
// subprocesses — there is no server registry to discover, and querying a
|
||||
// generic public relay for kind:10100 returns foreign/malformed events
|
||||
// (the "agent parse failed: missing field `name`" bug). The real agent
|
||||
// list comes from `list_managed_agents`; this overlay is simply empty.
|
||||
if state.is_serverless() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Query kind:10100 agent profile events from the relay.
|
||||
let events = query_relay(
|
||||
&state,
|
||||
|
||||
@@ -505,6 +505,19 @@ pub async fn unarchive_channel(
|
||||
#[tauri::command]
|
||||
pub async fn delete_channel(channel_id: String, state: State<'_, AppState>) -> Result<(), String> {
|
||||
let uuid = parse_channel_uuid(&channel_id)?;
|
||||
if state.is_serverless() {
|
||||
// No relay to process the kind:9008 delete command. Publish a NIP-09
|
||||
// (kind 5) deletion targeting the channel's addressable 39000/39002
|
||||
// coordinates so relays drop them. Only the owner (signer of those
|
||||
// events) can effectively delete.
|
||||
let my_pubkey = {
|
||||
let keys = state.keys.lock().map_err(|e| e.to_string())?;
|
||||
keys.public_key().to_hex()
|
||||
};
|
||||
let builder = events::build_delete_channel_serverless(&uuid.to_string(), &my_pubkey)?;
|
||||
submit_event(builder, &state).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let builder = events::build_delete_channel(uuid)?;
|
||||
submit_event(builder, &state).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -299,6 +299,22 @@ pub fn build_message_edit(
|
||||
Ok(EventBuilder::new(Kind::Custom(40003), content).tags(tags))
|
||||
}
|
||||
|
||||
/// Kind 5 — NIP-09 deletion of a serverless channel. Targets the addressable
|
||||
/// `a` coordinates of the channel's kind:39000 metadata and kind:39002 member
|
||||
/// list (both signed by `owner_pubkey_hex`), which generic relays honor.
|
||||
pub fn build_delete_channel_serverless(
|
||||
channel_id: &str,
|
||||
owner_pubkey_hex: &str,
|
||||
) -> Result<EventBuilder, String> {
|
||||
let meta_coord = format!("39000:{owner_pubkey_hex}:{channel_id}");
|
||||
let members_coord = format!("39002:{owner_pubkey_hex}:{channel_id}");
|
||||
let tags = vec![
|
||||
tag(vec!["a", &meta_coord])?,
|
||||
tag(vec!["a", &members_coord])?,
|
||||
];
|
||||
Ok(EventBuilder::new(Kind::Custom(5), "").tags(tags))
|
||||
}
|
||||
|
||||
/// Kind 5 — NIP-09 deletion (messages).
|
||||
pub fn build_delete_compat(target_event_id: EventId) -> Result<EventBuilder, String> {
|
||||
let tags = vec![tag(vec!["e", &target_event_id.to_hex()])?];
|
||||
@@ -999,6 +1015,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serverless_delete_channel_targets_addressable_coords() {
|
||||
// Deletion must reference both the 39000 metadata and 39002 members
|
||||
// addressable coordinates so a generic relay drops them.
|
||||
let event = sign(build_delete_channel_serverless("chan-del", PK_A).unwrap());
|
||||
assert_eq!(event.kind, Kind::Custom(5));
|
||||
let coords: Vec<String> = event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|t| {
|
||||
let s = t.as_slice();
|
||||
(s.first().map(String::as_str) == Some("a"))
|
||||
.then(|| s.get(1).cloned())
|
||||
.flatten()
|
||||
})
|
||||
.collect();
|
||||
assert!(coords.contains(&format!("39000:{PK_A}:chan-del")));
|
||||
assert!(coords.contains(&format!("39002:{PK_A}:chan-del")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serverless_members_with_roles_records_owner() {
|
||||
// The creator must be recorded as `owner` (4th element of the p tag) so
|
||||
|
||||
@@ -184,20 +184,45 @@ pub async fn submit_event_ws(
|
||||
.sign_with_keys(&keys)
|
||||
.map_err(|e| format!("failed to sign event: {e}"))?;
|
||||
|
||||
let futures = relay_urls
|
||||
.iter()
|
||||
.map(|url| submit_event_ws_one(&event, &keys, url));
|
||||
let results = futures_util::future::join_all(futures).await;
|
||||
// Publish to all relays, succeeding if any accepts. Public relays often
|
||||
// rate-limit bursts ("noting too much"); since the same event going to N
|
||||
// relays is idempotent (dedup by id), a transient rate-limit on one relay
|
||||
// is fine as long as another accepts. If ALL relays rate-limit, retry once
|
||||
// after a short backoff — adding an agent fires several writes in quick
|
||||
// succession, which is the usual trigger.
|
||||
for attempt in 0..2 {
|
||||
let futures = relay_urls
|
||||
.iter()
|
||||
.map(|url| submit_event_ws_one(&event, &keys, url));
|
||||
let results = futures_util::future::join_all(futures).await;
|
||||
|
||||
let mut last_err = None;
|
||||
for r in results {
|
||||
match r {
|
||||
Ok(resp) if resp.accepted => return Ok(resp),
|
||||
Ok(resp) => last_err = Some(format!("relay rejected event: {}", resp.message)),
|
||||
Err(e) => last_err = Some(e),
|
||||
let mut last_err = None;
|
||||
let mut all_rate_limited = !results.is_empty();
|
||||
for r in results {
|
||||
match r {
|
||||
Ok(resp) if resp.accepted => return Ok(resp),
|
||||
Ok(resp) => {
|
||||
if !resp.message.contains("rate-limit") {
|
||||
all_rate_limited = false;
|
||||
}
|
||||
last_err = Some(format!("relay rejected event: {}", resp.message));
|
||||
}
|
||||
Err(e) => {
|
||||
all_rate_limited = false;
|
||||
last_err = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only the rate-limit case is worth retrying; other rejections won't
|
||||
// change on retry.
|
||||
if attempt == 0 && all_rate_limited {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
|
||||
continue;
|
||||
}
|
||||
return Err(last_err.unwrap_or_else(|| "all relays failed".to_string()));
|
||||
}
|
||||
Err(last_err.unwrap_or_else(|| "all relays failed".to_string()))
|
||||
unreachable!("loop returns on both attempts")
|
||||
}
|
||||
|
||||
async fn submit_event_ws_one(
|
||||
|
||||
Reference in New Issue
Block a user