Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1362,9 +1362,9 @@ use deploy::{ensure_remote_provider_supported, resolve_deploy_model_provider};

#[path = "agents_profile.rs"]
mod profile;
pub(crate) use profile::*;
#[cfg(test)]
use profile::{profile_needs_sync, resolve_legacy_avatar};
pub(crate) use profile::{reconcile_agent_profile, ProfileReconcileData};

#[cfg(test)]
#[path = "agents_tests.rs"]
Expand Down
75 changes: 74 additions & 1 deletion desktop/src-tauri/src/commands/agents_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//! guard). Owns the reconcile data carrier, the legacy-avatar backfill, and
//! the needs-sync predicate.

use tauri::AppHandle;
use std::collections::HashSet;
use tauri::{AppHandle, Manager};

use crate::app_state::AppState;
use crate::managed_agents::managed_agent_avatar_url;
Expand Down Expand Up @@ -49,6 +50,78 @@ pub(super) fn resolve_legacy_avatar(
.unwrap_or_default()
}

pub(crate) fn profile_reconcile_data(
record: &crate::managed_agents::ManagedAgentRecord,
personas: &[crate::managed_agents::AgentDefinition],
) -> ProfileReconcileData {
ProfileReconcileData {
private_key_nsec: record.private_key_nsec.clone(),
name: record.name.clone(),
relay_url: record.relay_url.clone(),
avatar_url: record.avatar_url.clone(),
auth_tag: record.auth_tag.clone(),
pubkey: record.pubkey.clone(),
agent_command: crate::managed_agents::record_agent_command(record, personas),
persona_id: record.persona_id.clone(),
}
}

pub(crate) fn load_pending_profile_reconciliations(
app: &AppHandle,
) -> Result<Vec<(String, ProfileReconcileData)>, String> {
let state = app.state::<AppState>();
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|error| error.to_string())?;
let store_path = crate::managed_agents::managed_agents_store_path(app)?;
let queue_path = crate::migration::profile_reconcile_queue_path(&store_path);
if !queue_path.exists() {
return Ok(Vec::new());
}

let pending: HashSet<String> = crate::migration::read_profile_reconcile_queue(&queue_path)?
.into_iter()
.collect();
let records = crate::managed_agents::load_managed_agents(app)?;
let personas = crate::managed_agents::load_personas(app).unwrap_or_default();
let workspace_relay = relay_ws_url_with_override(&state);
Ok(records
.iter()
// A queue write deliberately precedes the migrated agent-store write.
// If the process dies between them, retain (but do not execute) the
// stale item until the next boot finishes renaming the record.
.filter(|record| {
pending.contains(&record.pubkey)
&& record.name == crate::managed_agents::POLLEN_DISPLAY_NAME
})
.map(|record| {
let mut data = profile_reconcile_data(record, &personas);
// Pin the effective relay while this workspace is active. Otherwise
// a fast community switch could make a queued task for A run on B.
data.relay_url =
crate::relay::effective_agent_relay_url(&data.relay_url, &workspace_relay);
(record.pubkey.clone(), data)
})
.collect())
}

pub(crate) fn mark_profile_reconciled(app: &AppHandle, pubkey: &str) -> Result<(), String> {
let state = app.state::<AppState>();
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|error| error.to_string())?;
let store_path = crate::managed_agents::managed_agents_store_path(app)?;
let queue_path = crate::migration::profile_reconcile_queue_path(&store_path);
if !queue_path.exists() {
return Ok(());
}
let mut pending = crate::migration::read_profile_reconcile_queue(&queue_path)?;
pending.retain(|queued| queued != pubkey);
Comment thread
klopez4212 marked this conversation as resolved.
Outdated
crate::migration::write_profile_reconcile_queue(&queue_path, &pending)
}

/// Reconcile an agent's kind:0 profile on the relay.
///
/// Queries the relay for the agent's existing profile and re-publishes if missing
Expand Down
4 changes: 4 additions & 0 deletions desktop/src-tauri/src/commands/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ pub async fn apply_workspace(

let state = restore_app.state::<AppState>();
super::agents::provider_access::reconcile_on_workspace_apply(&restore_app, &state).await?;
// The Bumble→Pollen migration may have renamed stopped agents. Reconcile
// their relay profiles independently of runtime restore; successful writes
// clear the durable queue, while failures retry on the next workspace apply.
crate::managed_agents::spawn_pending_profile_reconciliations(&restore_app);

// Backfill this exact relay+owner scope only after the workspace has been
// applied. Running at process boot would target the fallback relay and
Expand Down
24 changes: 17 additions & 7 deletions desktop/src-tauri/src/managed_agents/personas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ const FIZZ_SYSTEM_PROMPT: &str = "You are Fizz, an energetic maker who turns ide

const HONEY_SYSTEM_PROMPT: &str = "You are Honey, a warm and thoughtful communicator. Help users write clearly, organize ideas, brainstorm, summarize, and prepare for conversations. Be kind, creative, and concise. Add occasional bee wordplay or 🍯🐝—keep it sweet, never excessive.";

const BUMBLE_SYSTEM_PROMPT: &str = "You are Bumble, a curious and adventurous researcher. Explore questions, compare options, check assumptions, and explain what you find clearly. Be candid when uncertain and favor useful evidence. Add occasional bee wordplay or 🐝🔎—keep it playful, never chaotic.";
// Keep the published NIP-33 coordinate stable so existing Pollen agents and
// references are upgraded in place instead of being orphaned by the rename.
pub(crate) const POLLEN_PERSONA_ID: &str = "builtin:bumble";
pub(crate) const POLLEN_DISPLAY_NAME: &str = "Pollen";
pub(crate) const POLLEN_SYSTEM_PROMPT: &str = "You are Pollen, a curious and adventurous researcher. Explore questions, compare options, check assumptions, and explain what you find clearly. Be candid when uncertain and favor useful evidence. Add occasional bee wordplay or 🐝🔎—keep it playful, never chaotic.";
pub(crate) const POLLEN_LEGACY_DISPLAY_NAME: &str = "Bumble";
pub(crate) const POLLEN_LEGACY_SYSTEM_PROMPT: &str = "You are Bumble, a curious and adventurous researcher. Explore questions, compare options, check assumptions, and explain what you find clearly. Be candid when uncertain and favor useful evidence. Add occasional bee wordplay or 🐝🔎—keep it playful, never chaotic.";
// The embedded bytes are unchanged by the display-name migration. Keep the
// original storage symbol as the compatibility source and expose the current
// product name everywhere it is consumed.
const POLLEN_AVATAR: &str = BUMBLE_AVATAR;

const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[
BuiltInPersona {
Expand All @@ -32,7 +42,7 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[
avatar_url: Some(FIZZ_AVATAR),
system_prompt: FIZZ_SYSTEM_PROMPT,
name_pool: &[
"Nectar", "Comet", "Bramble", "Clover", "Pollen", "Amber", "Daisy", "Mason", "Thistle",
"Nectar", "Comet", "Bramble", "Clover", "Amber", "Daisy", "Mason", "Thistle",
"Waxwing", "Hive", "Meadow", "Juniper", "Aster", "Sage", "Willow", "Orchard", "Buzz",
],
model: None,
Expand All @@ -50,11 +60,11 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[
default_active: true,
},
BuiltInPersona {
id: "builtin:bumble",
display_name: "Bumble",
avatar_url: Some(BUMBLE_AVATAR),
system_prompt: BUMBLE_SYSTEM_PROMPT,
name_pool: &["Bumble"],
id: POLLEN_PERSONA_ID,
display_name: POLLEN_DISPLAY_NAME,
avatar_url: Some(POLLEN_AVATAR),
system_prompt: POLLEN_SYSTEM_PROMPT,
name_pool: &[POLLEN_DISPLAY_NAME],
model: None,
runtime: None,
default_active: true,
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/managed_agents/personas/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fn merge_personas_adds_missing_built_ins() {
.iter()
.map(|record| record.display_name.as_str())
.collect();
assert_eq!(display_names, vec!["Fizz", "Honey", "Bumble"]);
assert_eq!(display_names, vec!["Fizz", "Honey", "Pollen"]);
let active_ids: Vec<&str> = records
.iter()
.filter(|record| record.is_active)
Expand Down
48 changes: 48 additions & 0 deletions desktop/src-tauri/src/managed_agents/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,54 @@ pub async fn restore_managed_agents_on_launch(
Ok(())
}

pub(crate) fn spawn_pending_profile_reconciliations(app: &tauri::AppHandle) {
let state = app.state::<AppState>();
if !state
.managed_agent_profile_reconcile_enabled
.load(Ordering::Acquire)
{
return;
}
let items = match crate::commands::load_pending_profile_reconciliations(app) {
Ok(items) => items,
Err(error) => {
eprintln!("buzz-desktop: failed to load pending profile reconciliations: {error}");
return;
}
};

for (pubkey, data) in items {
let reconcile_app = app.clone();
tauri::async_runtime::spawn(async move {
let state = reconcile_app.state::<AppState>();
match crate::commands::reconcile_agent_profile(&state, &reconcile_app, &pubkey, &data)
.await
{
Ok(())
if state
.managed_agent_profile_reconcile_enabled
.load(Ordering::Acquire) =>
{
if let Err(error) =
crate::commands::mark_profile_reconciled(&reconcile_app, &pubkey)
{
eprintln!(
"buzz-desktop: failed to clear profile reconciliation for agent {pubkey}: {error}"
);
}
}
// Reconciliation can return Ok after the experiment is enabled
// mid-flight; retain the queue item rather than treating that
// intentional no-op as a successful relay write.
Ok(()) => {}
Err(error) => eprintln!(
"buzz-desktop: profile reconciliation failed for agent {pubkey}: {error}"
),
}
});
}
}

#[cfg(feature = "mesh-llm")]
fn persist_restore_error(
app: &tauri::AppHandle,
Expand Down
12 changes: 6 additions & 6 deletions desktop/src-tauri/src/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,11 @@ fn run_boot_migrations_inner(app: &tauri::AppHandle, reset_completed: bool) {
}
migrate_persona_provider_to_runtime(app);
reconcile_legacy_command_names(app);
// Fold personas.json into the unified store HERE: after the JSON-level
// personas.json migrations above (which must see the legacy file), and
// before every consumer of the load/save_personas shims below —
// sync_team_personas would otherwise operate on an empty definition set.
// Post-fold readers of the runtime map (`load_persona_runtimes`) fall
// back to the unified store's definitions.
// Fold personas.json after its JSON-level migrations and before consumers
// below; otherwise sync_team_personas sees an empty definition set.
// Post-fold runtime reads fall back to unified-store definitions.
fold_personas_into_agent_store(app);
pollen::migrate_pollen_agent_name(app);
// Clean the legacy baked team-instructions suffix out of stored prompts
// AFTER the fold (so definitions lifted out of personas.json are cleaned in
// the same boot) and BEFORE backfill_standalone_agents (so a manufactured
Expand Down Expand Up @@ -1376,6 +1374,8 @@ mod backfill;
pub use backfill::backfill_standalone_agents;
mod detach;
pub use detach::detach_directory_backed_teams;
mod pollen;
pub(crate) use pollen::*;
mod team_suffix;
pub use team_suffix::strip_baked_team_instructions;

Expand Down
Loading
Loading