Skip to content

Commit c14ab23

Browse files
author
npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw
committed
fix(desktop): reconcile agent profiles on boot restore
restore_managed_agents_on_launch never called reconcile_agent_profile, so agents restored at boot could have stale or missing kind:0 profiles on the relay. The UI start path (start_managed_agent) already did this as a fire-and-forget task. After Phase C writes PIDs/status to disk, collect ProfileReconcileData for each successfully spawned agent and spawn reconciliation tasks — same fire-and-forget pattern, same error logging, no blocking startup. Signed-off-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co>
1 parent 53ad127 commit c14ab23

2 files changed

Lines changed: 55 additions & 11 deletions

File tree

desktop/src-tauri/src/commands/agents.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -695,25 +695,25 @@ pub async fn create_managed_agent(
695695
}
696696

697697
/// Data needed for background profile reconciliation after agent start.
698-
struct ProfileReconcileData {
699-
private_key_nsec: String,
700-
name: String,
701-
relay_url: String,
698+
pub(crate) struct ProfileReconcileData {
699+
pub(crate) private_key_nsec: String,
700+
pub(crate) name: String,
701+
pub(crate) relay_url: String,
702702
/// Expected avatar URL for the published profile. `None` for legacy records
703703
/// that predate the `avatar_url` field — these will be backfilled from the
704704
/// relay's existing kind:0 profile on first reconciliation.
705-
avatar_url: Option<String>,
706-
auth_tag: Option<String>,
705+
pub(crate) avatar_url: Option<String>,
706+
pub(crate) auth_tag: Option<String>,
707707
/// The agent's pubkey (hex). Needed to update the persisted record during
708708
/// avatar backfill migration.
709-
pubkey: String,
709+
pub(crate) pubkey: String,
710710
/// The agent's command (e.g. "goose"). Used as fallback when no profile
711711
/// exists on the relay during avatar backfill.
712-
agent_command: String,
712+
pub(crate) agent_command: String,
713713
/// Persona ID if this agent was created from a persona. Used during avatar
714714
/// backfill to recover the correct avatar from the persona record when the
715715
/// relay profile has been corrupted.
716-
persona_id: Option<String>,
716+
pub(crate) persona_id: Option<String>,
717717
}
718718

719719
#[tauri::command]
@@ -877,7 +877,7 @@ fn resolve_legacy_avatar(
877877
/// Query and publish both target the agent's stored `relay_url` so that, under
878878
/// an active workspace relay override, reconciliation reads and writes the same
879879
/// relay the agent's profile actually lives on.
880-
async fn reconcile_agent_profile(
880+
pub(crate) async fn reconcile_agent_profile(
881881
state: &AppState,
882882
app: &AppHandle,
883883
agent_pubkey: &str,

desktop/src-tauri/src/managed_agents/restore.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ pub async fn restore_managed_agents_on_launch(
164164
.lock()
165165
.map_err(|error| error.to_string())?;
166166

167+
let mut successfully_spawned: Vec<String> = Vec::new();
168+
167169
for (pubkey, result) in spawn_results {
168170
let record = match find_managed_agent_mut(&mut records, &pubkey) {
169171
Ok(r) => r,
@@ -178,7 +180,8 @@ pub async fn restore_managed_agents_on_launch(
178180
record.last_stopped_at = None;
179181
record.last_exit_code = None;
180182
record.last_error = None;
181-
runtimes.insert(pubkey, ManagedAgentProcess { child, log_path });
183+
runtimes.insert(pubkey.clone(), ManagedAgentProcess { child, log_path });
184+
successfully_spawned.push(pubkey);
182185
}
183186
Err(error) => {
184187
record.updated_at = util::now_iso();
@@ -187,8 +190,49 @@ pub async fn restore_managed_agents_on_launch(
187190
}
188191
}
189192

193+
// Collect profile reconciliation data for successfully spawned agents before
194+
// releasing the lock. This mirrors the fire-and-forget pattern in
195+
// start_managed_agent — ensuring boot-restored agents get the same profile
196+
// self-healing as UI-started agents.
197+
let reconcile_items: Vec<(String, crate::commands::ProfileReconcileData)> =
198+
successfully_spawned
199+
.iter()
200+
.filter_map(|pubkey| {
201+
let record = records.iter().find(|r| r.pubkey == *pubkey)?;
202+
Some((
203+
pubkey.clone(),
204+
crate::commands::ProfileReconcileData {
205+
private_key_nsec: record.private_key_nsec.clone(),
206+
name: record.name.clone(),
207+
relay_url: record.relay_url.clone(),
208+
avatar_url: record.avatar_url.clone(),
209+
auth_tag: record.auth_tag.clone(),
210+
pubkey: record.pubkey.clone(),
211+
agent_command: record.agent_command.clone(),
212+
persona_id: record.persona_id.clone(),
213+
},
214+
))
215+
})
216+
.collect();
217+
190218
save_managed_agents(app, &records)?;
191219

220+
// ── Profile reconciliation (fire-and-forget) ────────────────────────────
221+
// Spawn background tasks to ensure each restored agent's kind:0 profile is
222+
// published on the relay. Same pattern as the UI start path.
223+
for (pubkey, data) in reconcile_items {
224+
let reconcile_app = app.clone();
225+
tauri::async_runtime::spawn(async move {
226+
let state = reconcile_app.state::<AppState>();
227+
if let Err(e) =
228+
crate::commands::reconcile_agent_profile(&state, &reconcile_app, &pubkey, &data)
229+
.await
230+
{
231+
eprintln!("sprout-desktop: profile reconciliation failed for agent {pubkey}: {e}");
232+
}
233+
});
234+
}
235+
192236
Ok(())
193237
}
194238

0 commit comments

Comments
 (0)