Skip to content
2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ zip = "8"
flate2 = "1"
sherpa-onnx = "1.12"
regex = "1"
rusqlite = { version = "0.37", features = ["bundled"] }
rusqlite = { version = "0.37", features = ["bundled", "backup"] }
axum = "0.8"
rodio = "0.22"
earshot = "1.0"
Expand Down
24 changes: 10 additions & 14 deletions desktop/src-tauri/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ pub struct AppState {
pub huddle_audio: crate::huddle::tts_settings::HuddleAudioSettingsState,
/// Tauri app handle — stored after setup so huddle commands can emit
/// `huddle-state-changed` events without needing the handle threaded
/// through every call site.
///
/// Set once during `setup()` in `lib.rs`; never cleared.
/// through every call site. Set once during `setup()` in `lib.rs`;
/// never cleared.
pub app_handle: Mutex<Option<AppHandle>>,
/// Port of the localhost media streaming proxy (set during setup).
pub media_proxy_port: AtomicU16,
Expand All @@ -68,13 +67,8 @@ pub struct AppState {
/// signing commands check this flag via [`AppState::signing_keys`] and
/// return `Err` so no events are published under the inaccessible identity.
/// Mutually exclusive with `identity_lost` (guaranteed by `RecoveryState`
/// at the resolve boundary).
///
/// Ordering: writers store with `Ordering::Release` after `state.keys` is
/// updated, so a reader observing `false` with `Ordering::Acquire` is
/// guaranteed to see the updated keys. Writers: `setup()` (initial
/// resolution via `resolve_persisted_identity`) and `import_identity`
/// (clears the flag when the user successfully imports a new key).
/// at the resolve boundary). Ordering: writers store with
/// `Ordering::Release`; readers use `Ordering::Acquire`.
pub keyring_locked: AtomicBool,
/// Set when identity resolution detected a "lost" state: the migration
/// marker was present but the keyring was empty and no plaintext fallback
Expand All @@ -97,11 +91,12 @@ pub struct AppState {
/// Set when the boot-time Phase 2 reset attempted a wipe but verification
/// failed. The sentinel is preserved so the next relaunch retries. All
/// identity-dependent setup is skipped; the frontend shows a reset-failed
/// recovery screen via `get_identity`.
///
/// Ordering: written once in `setup()` with `Ordering::Release`; read in
/// `get_identity` with `Ordering::Acquire`.
/// recovery screen via `get_identity`. Written once in `setup()` with
/// `Ordering::Release`; read in `get_identity` with `Ordering::Acquire`.
pub reset_failed: AtomicBool,
/// Set when pre-migration file-commit recovery fails; all store-touching
/// setup is skipped. Same write/read ordering as `reset_failed`.
pub store_recovery_failed: AtomicBool,
/// Cached ACP session config from running agents, keyed by canonical
/// `(agent pubkey, relay URL)` runtime identity.
/// Populated when the harness emits `session_config_captured` observer events.
Expand Down Expand Up @@ -226,6 +221,7 @@ pub fn build_app_state() -> AppState {
keyring_locked: AtomicBool::new(false),
identity_lost: AtomicBool::new(false),
reset_failed: AtomicBool::new(false),
store_recovery_failed: AtomicBool::new(false),
#[cfg(feature = "mesh-llm")]
mesh_llm_runtime: AsyncMutex::new(None),
#[cfg(feature = "mesh-llm")]
Expand Down
33 changes: 19 additions & 14 deletions desktop/src-tauri/src/commands/agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
},
},
current_instance_id, is_reserved_env_key, is_safe_to_reveal, is_well_formed_env_key,
known_acp_runtime, load_managed_agents, load_personas, save_managed_agents,
known_acp_runtime, load_managed_agents, load_personas, mutate_agent_store,
sync_managed_agent_processes, AgentDefinition, GlobalAgentConfig, KnownAcpRuntime,
ManagedAgentRecord, ManagedAgentRuntimeKey, MAX_ENV_VALUE_BYTES,
},
Expand Down Expand Up @@ -252,27 +252,32 @@ pub async fn get_agent_config_surface(
state: State<'_, AppState>,
) -> Result<RuntimeConfigSurface, String> {
let record = {
let _store_guard = state
let store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|e| e.to_string())?;
let mut records = load_managed_agents(&app)?;
let mut runtimes = state
.managed_agent_processes
.lock()
.map_err(|e| e.to_string())?;
let (sync_changed, exited_pubkeys) =
sync_managed_agent_processes(&mut records, &mut runtimes, &current_instance_id(&app));
if sync_changed {
save_managed_agents(&app, &records)?;
}
for pubkey in &exited_pubkeys {
state.clear_agent_session_caches(pubkey);
let instance_id = current_instance_id(&app);

let pubkey_for_closure = pubkey.clone();
let ((record, exited_pubkeys), _guard) =
mutate_agent_store(&app, store_guard, move |mut instances, _journal| {
let (_, exited) =
sync_managed_agent_processes(&mut instances, &mut runtimes, &instance_id);
let record = instances
.iter()
.find(|r| r.pubkey == pubkey_for_closure)
.ok_or_else(|| format!("agent {pubkey_for_closure} not found"))?
.clone();
Ok((instances, (record, exited)))
})?;
for pk in &exited_pubkeys {
state.clear_agent_session_caches(pk);
}
records
.into_iter()
.find(|r| r.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?
record
};

let personas = load_personas(&app).unwrap_or_default();
Expand Down
138 changes: 69 additions & 69 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,9 +519,9 @@ async fn restart_single_agent_after_install(
app_state::AppState,
managed_agents::{
agent_readiness, current_instance_id, find_managed_agent_mut, known_acp_runtime,
load_global_agent_config, load_managed_agents, load_personas, record_agent_command,
resolve_effective_agent_env, save_managed_agents, stop_managed_agent_process,
sync_managed_agent_processes, AgentReadiness, BackendKind,
load_global_agent_config, load_personas, mutate_agent_store, record_agent_command,
resolve_effective_agent_env, stop_managed_agent_process, sync_managed_agent_processes,
AgentReadiness, BackendKind,
},
};
use tauri::Manager;
Expand All @@ -533,81 +533,77 @@ async fn restart_single_agent_after_install(
let stop_result = tokio::task::spawn_blocking(move || {
let state = app_for_stop.state::<AppState>();

let _store_guard = state
let store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|e| format!("failed to acquire store lock: {e}"))?;

let mut records = load_managed_agents(&app_for_stop)?;
let mut runtimes = state
.managed_agent_processes
.lock()
.map_err(|e| format!("failed to acquire runtimes lock: {e}"))?;

// Sync process state so PID liveness reflects current reality.
let (sync_changed, _) = sync_managed_agent_processes(
&mut records,
&mut runtimes,
&current_instance_id(&app_for_stop),
);
if sync_changed {
save_managed_agents(&app_for_stop, &records)?;
}

// Re-verify eligibility under lock.
let record = records
.iter()
.find(|r| r.pubkey == pubkey_owned)
.ok_or_else(|| format!("agent {pubkey_owned} not found"))?;
let app_for_closure = app_for_stop.clone();
let (runtime_keys, _guard) = mutate_agent_store(
&app_for_stop,
store_guard,
move |mut records, _journal| {
let instance_id = current_instance_id(&app_for_closure);
// Sync process state so PID liveness reflects current reality.
sync_managed_agent_processes(&mut records, &mut runtimes, &instance_id);

// Re-verify eligibility under lock.
let record = records
.iter()
.find(|r| r.pubkey == pubkey_owned)
.ok_or_else(|| format!("agent {pubkey_owned} not found"))?;

if record.backend != BackendKind::Local {
return Err(format!("agent {pubkey_owned} is no longer a local agent"));
}
let runtime_keys =
crate::managed_agents::managed_agent_runtime_keys(&runtimes, &pubkey_owned);
if runtime_keys.is_empty() {
return Err(format!(
"agent {pubkey_owned} no longer has a live pair runtime after sync"
));
}
if record.backend != BackendKind::Local {
return Err(format!("agent {pubkey_owned} is no longer a local agent"));
}
let runtime_keys =
crate::managed_agents::managed_agent_runtime_keys(&runtimes, &pubkey_owned);
if runtime_keys.is_empty() {
return Err(format!("agent {pubkey_owned} no longer has a live pair runtime after sync"));
}

let personas = load_personas(&app_for_stop).unwrap_or_default();
let global = load_global_agent_config(&app_for_stop).unwrap_or_default();
let personas = load_personas(&app_for_closure).unwrap_or_default();
let global = load_global_agent_config(&app_for_closure).unwrap_or_default();

let effective_cmd = record_agent_command(record, &personas);
let runtime_matches =
known_acp_runtime(&effective_cmd).is_some_and(|r| r.id == runtime_id_owned);
if !runtime_matches {
return Err(format!(
"agent {pubkey_owned} runtime no longer matches {runtime_id_owned} under lock"
));
}
let effective_cmd = record_agent_command(record, &personas);
let runtime_matches =
known_acp_runtime(&effective_cmd).is_some_and(|r| r.id == runtime_id_owned);
if !runtime_matches {
return Err(format!(
"agent {pubkey_owned} runtime no longer matches {runtime_id_owned} under lock"
));
}

let setup_mode = runtimes
.iter()
.find(|(key, _)| key.pubkey == pubkey_owned)
.map(|(_, p)| p.setup_mode)
.unwrap_or(false);
if !setup_mode {
return Err(format!(
"agent {pubkey_owned} is not in setup mode under lock — skipping"
));
}
let setup_mode = runtimes
.iter()
.find(|(key, _)| key.pubkey == pubkey_owned)
.map(|(_, p)| p.setup_mode)
.unwrap_or(false);
if !setup_mode {
return Err(format!("agent {pubkey_owned} is not in setup mode under lock — skipping"));
}

let runtime_meta = known_acp_runtime(&effective_cmd);
let effective = resolve_effective_agent_env(record, &personas, runtime_meta, &global);
if !matches!(agent_readiness(&effective), AgentReadiness::Ready) {
return Err(format!(
"agent {pubkey_owned} readiness is still NotReady after install — not bouncing"
));
}
let runtime_meta = known_acp_runtime(&effective_cmd);
let effective = resolve_effective_agent_env(record, &personas, runtime_meta, &global);
if !matches!(agent_readiness(&effective), AgentReadiness::Ready) {
return Err(format!(
"agent {pubkey_owned} readiness is still NotReady after install — not bouncing"
));
}

// Stop the process.
let record_mut = find_managed_agent_mut(&mut records, &pubkey_owned)?;
stop_managed_agent_process(&app_for_stop, record_mut, &mut runtimes)?;
save_managed_agents(&app_for_stop, &records)?;
// Stop the process — mutation happens under the advisory lock.
let record_mut = find_managed_agent_mut(&mut records, &pubkey_owned)?;
stop_managed_agent_process(&app_for_closure, record_mut, &mut runtimes)?;

Ok(runtime_keys)
Ok((records, runtime_keys))
},
)?;
Ok::<Vec<crate::managed_agents::ManagedAgentRuntimeKey>, String>(runtime_keys)
})
.await;

Expand Down Expand Up @@ -659,19 +655,23 @@ fn persist_last_error_on_install(
) -> Result<(), String> {
use crate::{
app_state::AppState,
managed_agents::{find_managed_agent_mut, load_managed_agents, save_managed_agents},
managed_agents::{find_managed_agent_mut, mutate_agent_store},
};
use tauri::Manager;
let state = app.state::<AppState>();
let _store_guard = state
let store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|e| format!("failed to acquire store lock: {e}"))?;
let mut records = load_managed_agents(app)?;
let record = find_managed_agent_mut(&mut records, pubkey)?;
record.last_error = Some(error.to_string());
record.updated_at = crate::util::now_iso();
save_managed_agents(app, &records)
let pubkey = pubkey.to_owned();
let error = error.to_owned();
mutate_agent_store(app, store_guard, move |mut instances, _journal| {
let record = find_managed_agent_mut(&mut instances, &pubkey)?;
record.last_error = Some(error);
record.updated_at = crate::util::now_iso();
Ok((instances, ()))
})
.map(|_| ())
}

/// Build the `-l -c` argument list for the install shell.
Expand Down
Loading
Loading