fix: migrate existing persona-backed agents to clear stale prompt/model snapshots

Existing agents created before this PR have persona system_prompt and model
values snapshotted onto the agent record. With the new precedence logic
(agent record wins when set), those stale snapshots would be treated as
explicit user overrides, blocking persona updates from taking effect.

Migration: on load, clear system_prompt and model for any record where
persona_id is set. Safe because the old code ignored agent-level overrides
for persona-backed agents — no user has ever set a meaningful override.
Persists after first run so it only executes once.

Also: replace silent unwrap_or_default() on persona-load errors with
eprintln! warnings so corrupted persona files don't silently spawn agents
with no prompt.
This commit is contained in:
Taylor Ho
2026-05-26 20:51:37 -07:00
parent 848d9823f9
commit f81f73f629
3 changed files with 44 additions and 3 deletions

View File

@@ -46,7 +46,13 @@ fn build_deploy_payload(
// Resolve system prompt and model via the persona (same as local spawn).
// Re-reads the persona store so edits take effect without recreating the agent.
let personas = load_personas(app).unwrap_or_default();
let personas = load_personas(app).unwrap_or_else(|e| {
eprintln!(
"sprout-desktop: failed to load personas for deploy payload (agent {}) — proceeding without persona context: {e}",
record.name
);
Vec::new()
});
let (effective_prompt, effective_model) =
crate::managed_agents::resolve_effective_prompt_and_model(
record.persona_id.as_deref(),

View File

@@ -672,7 +672,13 @@ pub fn spawn_agent_child(
// Resolve system prompt and model via the extracted helper. Re-reads
// the persona store at spawn time so edits take effect without
// deleting and recreating the agent.
let personas = super::load_personas(app).unwrap_or_default();
let personas = super::load_personas(app).unwrap_or_else(|e| {
eprintln!(
"sprout-desktop: failed to load personas for agent {} — spawning with no persona context: {e}",
record.name
);
Vec::new()
});
let (effective_prompt, effective_model) = resolve_effective_prompt_and_model(
record.persona_id.as_deref(),
&personas,

View File

@@ -40,7 +40,36 @@ pub fn load_managed_agents(app: &AppHandle) -> Result<Vec<ManagedAgentRecord>, S
let content = fs::read_to_string(&path)
.map_err(|error| format!("failed to read agent store: {error}"))?;
serde_json::from_str(&content).map_err(|error| format!("failed to parse agent store: {error}"))
let mut records: Vec<ManagedAgentRecord> =
serde_json::from_str(&content).map_err(|error| format!("failed to parse agent store: {error}"))?;
// One-time migration: clear stale system_prompt/model snapshots on
// persona-backed agents. Before this fix, creation snapshotted the
// persona's values onto the agent record. With the new precedence logic
// (agent record wins when set), those stale snapshots would be treated as
// explicit user overrides and block persona updates from taking effect.
//
// Safe to clear unconditionally: the old code ignored agent-level overrides
// for persona-backed agents, so no user has ever set a meaningful override.
let mut migrated = false;
for record in &mut records {
if record.persona_id.is_some() {
if record.system_prompt.is_some() {
record.system_prompt = None;
migrated = true;
}
if record.model.is_some() {
record.model = None;
migrated = true;
}
}
}
if migrated {
// Persist the migration so it only runs once.
save_managed_agents(app, &records)?;
}
Ok(records)
}
pub fn save_managed_agents(app: &AppHandle, records: &[ManagedAgentRecord]) -> Result<(), String> {