Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 12 additions & 22 deletions desktop/src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ pub(super) fn tombstone_managed_agent_pending(
},
};
use buzz_core_pkg::kind::KIND_MANAGED_AGENT;
use nostr::JsonUtil;

const KIND_DELETE: u32 = 5;

Expand All @@ -97,17 +96,12 @@ pub(super) fn tombstone_managed_agent_pending(
delete_retained_event(&conn, KIND_MANAGED_AGENT, &owner_pubkey, agent_pubkey)?;
retain_event(
&conn,
&RetainedEvent {
kind: KIND_DELETE,
pubkey: owner_pubkey,
// Key by the target coordinate so cross-kind d-tag tombstones
// occupy distinct rows (F2c).
d_tag: tombstone_retention_d_tag(KIND_MANAGED_AGENT, agent_pubkey),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
},
&RetainedEvent::pending(
KIND_DELETE,
owner_pubkey,
tombstone_retention_d_tag(KIND_MANAGED_AGENT, agent_pubkey),
&event,
),
)
})();
if let Err(e) = result {
Expand Down Expand Up @@ -176,7 +170,6 @@ pub(super) fn build_agent_archive_request(
pub(super) fn archive_managed_agent_pending(app: &AppHandle, state: &AppState, agent_pubkey: &str) {
use crate::managed_agents::retention::{open_retention_db, retain_event, RetainedEvent};
use buzz_core_pkg::kind::KIND_IA_ARCHIVE_REQUEST;
use nostr::JsonUtil;

let result = (|| -> Result<(), String> {
let scope = crate::managed_agents::retention::active_retention_scope(app, state)?;
Expand All @@ -185,15 +178,12 @@ pub(super) fn archive_managed_agent_pending(app: &AppHandle, state: &AppState, a
let conn = open_retention_db(&scope.db_path)?;
retain_event(
&conn,
&RetainedEvent {
kind: KIND_IA_ARCHIVE_REQUEST,
pubkey: owner_pubkey,
d_tag: agent_pubkey.to_string(),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
},
&RetainedEvent::pending(
KIND_IA_ARCHIVE_REQUEST,
owner_pubkey,
agent_pubkey.to_string(),
&event,
),
)
})();
if let Err(e) = result {
Expand Down
42 changes: 15 additions & 27 deletions desktop/src-tauri/src/commands/personas/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ mod inbound_tests;
///
/// The retention store decides whether the inbound event wins over a pending
/// local edit (`retain_inbound_event`): `personas.json` is only patched when the
/// retain reports [`InboundOutcome::Applied`], so an equal-second collision with
/// a pending local edit leaves the local record — and its queued publish —
/// untouched.
/// retain reports [`InboundOutcome::Applied`], so an inbound event colliding
/// with a pending local edit — at any timestamp, newer included — leaves the
/// local record and its queued publish untouched.
///
/// `arrival_relay_url` is the relay the calling subscription is bound to. The
/// retention store this event belongs to is decided by the community that
Expand Down Expand Up @@ -78,7 +78,6 @@ fn reconcile_inbound_persona_event_blocking(
team_events::team_content_from_event,
};
use buzz_core_pkg::kind::{KIND_DELETION, KIND_MANAGED_AGENT, KIND_PERSONA, KIND_TEAM};
use nostr::JsonUtil;

let state = app.state::<AppState>();
let event = parse_verified_inbound_event(&event_json)?;
Expand Down Expand Up @@ -133,17 +132,9 @@ fn reconcile_inbound_persona_event_blocking(
let conn = open_retention_db(&scope.db_path)?;
let outcome = retain_inbound_event(
&conn,
&RetainedEvent {
kind,
pubkey: event.pubkey.to_hex(),
d_tag: d_tag.clone(),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: false,
},
&RetainedEvent::inbound(kind, event.pubkey.to_hex(), d_tag.clone(), &event),
)?;
if outcome == InboundOutcome::Skipped {
if outcome != InboundOutcome::Applied {
return Ok(());
}

Expand Down Expand Up @@ -245,7 +236,6 @@ fn reconcile_inbound_tombstone(
save_managed_agents, save_teams,
};
use buzz_core_pkg::kind::{KIND_DELETION, KIND_MANAGED_AGENT, KIND_PERSONA, KIND_TEAM};
use nostr::JsonUtil;

let Some((target_kind, target_d_tag)) = parse_deletion_coordinate(event) else {
return Ok(()); // no routable coordinate — nothing to delete
Expand All @@ -260,8 +250,9 @@ fn reconcile_inbound_tombstone(
.map_err(|error| error.to_string())?;

// Resolve against the retained tombstone row (keyed by the target
// coordinate, F2c) so a re-received tombstone or one older than a pending
// local edit is a no-op. Scoped to the arrival community, so a workspace
// coordinate, F2c) so a re-received tombstone, one that loses the ordering
// compare, or one landing on a pending tombstone of its own is a no-op.
// Scoped to the arrival community, so a workspace
// switch since arrival drops the tombstone instead of retaining it — and
// deleting a record — in the wrong community's store.
let Some(scope) =
Expand All @@ -272,17 +263,14 @@ fn reconcile_inbound_tombstone(
let conn = open_retention_db(&scope.db_path)?;
let outcome = retain_inbound_event(
&conn,
&RetainedEvent {
kind: KIND_DELETION,
pubkey: event.pubkey.to_hex(),
d_tag: tombstone_retention_d_tag(target_kind, &target_d_tag),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: false,
},
&RetainedEvent::inbound(
KIND_DELETION,
event.pubkey.to_hex(),
tombstone_retention_d_tag(target_kind, &target_d_tag),
event,
),
)?;
if outcome == InboundOutcome::Skipped {
if outcome != InboundOutcome::Applied {
return Ok(());
}

Expand Down
25 changes: 6 additions & 19 deletions desktop/src-tauri/src/commands/personas/pending.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ pub(super) fn prepare_persona_publication_at(
retention::{get_retained_event, open_retention_db, retain_event, RetainedEvent},
};
use buzz_core_pkg::kind::KIND_PERSONA;
use nostr::JsonUtil;

let d_tag = persona_d_tag(persona);
let pubkey = keys.public_key().to_hex();
Expand All @@ -171,15 +170,7 @@ pub(super) fn prepare_persona_publication_at(
))
.sign_with_keys(keys)
.map_err(|e| format!("failed to sign persona event: {e}"))?;
let retained = RetainedEvent {
kind: KIND_PERSONA,
pubkey,
d_tag,
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
};
let retained = RetainedEvent::pending(KIND_PERSONA, pubkey, d_tag, &event);
retain_event(&conn, &retained)?;
Ok((event, retained, scoped_persona))
}
Expand Down Expand Up @@ -209,7 +200,6 @@ pub(in crate::commands) fn tombstone_persona_pending(
},
};
use buzz_core_pkg::kind::KIND_PERSONA;
use nostr::JsonUtil;

const KIND_DELETE: u32 = 5;

Expand All @@ -225,17 +215,14 @@ pub(in crate::commands) fn tombstone_persona_pending(
delete_retained_event(&conn, KIND_PERSONA, &pubkey, d_tag)?;
retain_event(
&conn,
&RetainedEvent {
kind: KIND_DELETE,
&RetainedEvent::pending(
KIND_DELETE,
pubkey,
// Key by the target coordinate so cross-kind d-tag tombstones
// occupy distinct rows (F2c).
d_tag: tombstone_retention_d_tag(KIND_PERSONA, d_tag),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
},
tombstone_retention_d_tag(KIND_PERSONA, d_tag),
&event,
),
)
})();
if let Err(e) = result {
Expand Down
16 changes: 6 additions & 10 deletions desktop/src-tauri/src/commands/personas/snapshot/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,6 @@ fn retain_agent_pending(app: &AppHandle, state: &AppState, record: &ManagedAgent
retention::{get_retained_event, open_retention_db, retain_event, RetainedEvent},
};
use buzz_core_pkg::kind::KIND_MANAGED_AGENT;
use nostr::JsonUtil;

let result = (|| -> Result<(), String> {
let scope = crate::managed_agents::retention::active_retention_scope(app, state)?;
Expand All @@ -667,15 +666,12 @@ fn retain_agent_pending(app: &AppHandle, state: &AppState, record: &ManagedAgent
};
retain_event(
&conn,
&RetainedEvent {
kind: KIND_MANAGED_AGENT,
pubkey: owner_pubkey,
d_tag: record.pubkey.clone(),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
},
&RetainedEvent::pending(
KIND_MANAGED_AGENT,
owner_pubkey,
record.pubkey.clone(),
&event,
),
)
})();
if let Err(e) = result {
Expand Down
16 changes: 6 additions & 10 deletions desktop/src-tauri/src/commands/team_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,6 @@ fn retain_agent_pending(app: &AppHandle, state: &AppState, record: &ManagedAgent
retention::{get_retained_event, open_retention_db, retain_event, RetainedEvent},
};
use buzz_core_pkg::kind::KIND_MANAGED_AGENT;
use nostr::JsonUtil;

let result = (|| -> Result<(), String> {
let scope = crate::managed_agents::retention::active_retention_scope(app, state)?;
Expand All @@ -877,15 +876,12 @@ fn retain_agent_pending(app: &AppHandle, state: &AppState, record: &ManagedAgent
};
retain_event(
&conn,
&RetainedEvent {
kind: KIND_MANAGED_AGENT,
pubkey: owner_pubkey,
d_tag: record.pubkey.clone(),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
},
&RetainedEvent::pending(
KIND_MANAGED_AGENT,
owner_pubkey,
record.pubkey.clone(),
&event,
),
)
})();
if let Err(e) = result {
Expand Down
25 changes: 6 additions & 19 deletions desktop/src-tauri/src/commands/teams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ pub(super) fn retain_team_pending(app: &AppHandle, state: &AppState, team: &Team
team_events::build_team_event,
};
use buzz_core_pkg::kind::KIND_TEAM;
use nostr::JsonUtil;

let result = (|| -> Result<(), String> {
let scope = crate::managed_agents::retention::active_retention_scope(app, state)?;
Expand All @@ -59,15 +58,7 @@ pub(super) fn retain_team_pending(app: &AppHandle, state: &AppState, team: &Team
.map_err(|e| format!("failed to sign team event: {e}"))?;
retain_event(
&conn,
&RetainedEvent {
kind: KIND_TEAM,
pubkey,
d_tag: team.id.clone(),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
},
&RetainedEvent::pending(KIND_TEAM, pubkey, team.id.clone(), &event),
)
})();
if let Err(e) = result {
Expand All @@ -93,7 +84,6 @@ fn tombstone_team_pending(app: &AppHandle, state: &AppState, d_tag: &str) {
team_events::build_team_delete,
};
use buzz_core_pkg::kind::KIND_TEAM;
use nostr::JsonUtil;

const KIND_DELETE: u32 = 5;

Expand All @@ -107,17 +97,14 @@ fn tombstone_team_pending(app: &AppHandle, state: &AppState, d_tag: &str) {
delete_retained_event(&conn, KIND_TEAM, &pubkey, d_tag)?;
retain_event(
&conn,
&RetainedEvent {
kind: KIND_DELETE,
&RetainedEvent::pending(
KIND_DELETE,
pubkey,
// Key by the target coordinate so cross-kind d-tag tombstones
// occupy distinct rows (F2c).
d_tag: tombstone_retention_d_tag(KIND_TEAM, d_tag),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
},
tombstone_retention_d_tag(KIND_TEAM, d_tag),
&event,
),
)
})();
if let Err(e) = result {
Expand Down
39 changes: 18 additions & 21 deletions desktop/src-tauri/src/event_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,35 @@ pub fn run_event_sync(app: &tauri::AppHandle, owner_keys: &nostr::Keys, db_path:
/// `AppState::keys` mutex. The reconcile itself is still synchronous JSON,
/// SQLite, and signing work, so it runs on the blocking pool rather than an
/// async worker.
///
/// # Ordering: reconcile, then barrier, and both before the flush publishes
///
/// The reconcile marks changed coordinates `pending_sync = 1`; the boot barrier
/// ([`crate::managed_agents::config_barrier::run_boot_barrier`]) then decides
/// which of those may actually go out and gates the rest. Running the barrier
/// after the reconcile is what lets it see the rows the reconcile just queued —
/// the stale-store republish it exists to suppress is created right here. The
/// flush loop's first tick is 30s of relay lookups away, so in practice the gate
/// is set before anything publishes; if a flush did win the race, the barrier
/// still gates every later sweep.
pub fn spawn_event_sync(
app: tauri::AppHandle,
owner_keys: nostr::Keys,
db_path: std::path::PathBuf,
) {
tauri::async_runtime::spawn(async move {
let barrier_app = app.clone();
if let Err(e) = tauri::async_runtime::spawn_blocking(move || {
run_event_sync(&app, &owner_keys, &db_path);
})
.await
{
eprintln!("buzz-desktop: event-sync: spawn_blocking failed: {e}");
// The barrier still runs: a failed reconcile leaves whatever rows
// a previous boot queued, and gating those is exactly the case the
// barrier exists for.
}
crate::managed_agents::config_barrier::run_boot_barrier(&barrier_app).await;
});
}

Expand Down Expand Up @@ -186,17 +202,7 @@ fn migrate_personas_in_dir_at(
continue;
}

let retained = RetainedEvent {
kind: KIND_PERSONA,
pubkey: pubkey.clone(),
d_tag,
content: event_content,
// Safety: nostr timestamps are seconds and stay below i64::MAX
// until year 2262.
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
};
let retained = RetainedEvent::pending(KIND_PERSONA, pubkey.clone(), d_tag, &event);

// The monotonic bump guarantees `created_at > head`, so the upsert's
// `>=` guard always lands the UPDATE — `migrated` counts only real,
Expand Down Expand Up @@ -259,7 +265,6 @@ fn migrate_teams_in_dir_at(
TeamRecord,
};
use buzz_core_pkg::kind::KIND_TEAM;
use nostr::JsonUtil;

let pubkey = keys.public_key().to_hex();

Expand Down Expand Up @@ -312,15 +317,7 @@ fn migrate_teams_in_dir_at(
continue;
}

let retained = RetainedEvent {
kind: KIND_TEAM,
pubkey: pubkey.clone(),
d_tag,
content: event_content,
created_at: event.created_at.as_secs() as i64,
raw_event: event.as_json(),
pending_sync: true,
};
let retained = RetainedEvent::pending(KIND_TEAM, pubkey.clone(), d_tag, &event);

// Monotonic bump guarantees the upsert UPDATE lands — `migrated` counts
// only real republishes.
Expand Down
Loading
Loading