fix: add database migration for backend column and fix agent selection race condition

- Add run_migrations() to SQLite store that adds 'backend' column to existing
  missions tables (CREATE TABLE IF NOT EXISTS doesn't add columns to existing tables)
- Fix race condition in new-mission-dialog when switching backends: wait for
  backendAgents to finish loading before setting default agent, and only use
  fallback agents for opencode backend
This commit is contained in:
Thomas Marchand
2026-01-18 11:55:26 +00:00
parent 699473576b
commit fbf2715e1e
2 changed files with 38 additions and 5 deletions

View File

@@ -65,7 +65,7 @@ export function NewMissionDialog({
});
// SWR: fetch agents for selected backend
const { data: backendAgents } = useSWR<BackendAgent[]>(
const { data: backendAgents, isLoading: backendAgentsLoading } = useSWR<BackendAgent[]>(
newMissionBackend ? `backend-${newMissionBackend}-agents` : null,
() => listBackendAgents(newMissionBackend),
{ revalidateOnFocus: false, dedupingInterval: 30000 }
@@ -81,8 +81,11 @@ export function NewMissionDialog({
dedupingInterval: 30000,
});
// Parse agents from either backend API or fallback
const agents = backendAgents?.map(a => a.name) || parseAgentNames(agentsPayload);
// Parse agents from backend API (only use fallback for opencode backend)
// For non-opencode backends, wait for backendAgents to load to avoid race condition
const agents = newMissionBackend === 'opencode'
? (backendAgents?.map(a => a.name) || parseAgentNames(agentsPayload))
: (backendAgents?.map(a => a.name) || []);
const formatWorkspaceType = (type: Workspace['workspace_type']) =>
type === 'host' ? 'host' : 'isolated';
@@ -104,9 +107,13 @@ export function NewMissionDialog({
// Set default agent when dialog opens (only once per open)
// Wait for both agents AND config to load before setting defaults
useEffect(() => {
if (!open || defaultSet || agents.length === 0) return;
if (!open || defaultSet) return;
// Wait for config to finish loading (undefined = still loading, null/object = loaded)
if (config === undefined) return;
// Wait for backend agents to finish loading (avoid race condition when switching backends)
if (backendAgentsLoading) return;
// If no agents available yet, wait
if (agents.length === 0) return;
if (config?.default_agent && agents.includes(config.default_agent)) {
setNewMissionAgent(config.default_agent);
@@ -114,7 +121,7 @@ export function NewMissionDialog({
setNewMissionAgent('Sisyphus');
}
setDefaultSet(true);
}, [open, defaultSet, agents, config]);
}, [open, defaultSet, agents, config, backendAgentsLoading]);
const resetForm = () => {
setNewMissionWorkspace('');

View File

@@ -106,6 +106,9 @@ impl SqliteMissionStore {
conn.execute_batch(SCHEMA)
.map_err(|e| format!("Failed to run schema: {}", e))?;
// Run migrations for existing databases
Self::run_migrations(&conn)?;
Ok::<_, String>(conn)
})
.await
@@ -155,6 +158,29 @@ impl SqliteMissionStore {
String::new()
}
}
/// Run database migrations for existing databases.
/// CREATE TABLE IF NOT EXISTS doesn't add columns to existing tables,
/// so we need to handle schema changes manually.
fn run_migrations(conn: &Connection) -> Result<(), String> {
// Check if 'backend' column exists in missions table
let has_backend_column: bool = conn
.prepare("SELECT 1 FROM pragma_table_info('missions') WHERE name = 'backend'")
.map_err(|e| format!("Failed to check for backend column: {}", e))?
.exists([])
.map_err(|e| format!("Failed to query table info: {}", e))?;
if !has_backend_column {
tracing::info!("Running migration: adding 'backend' column to missions table");
conn.execute(
"ALTER TABLE missions ADD COLUMN backend TEXT NOT NULL DEFAULT 'opencode'",
[],
)
.map_err(|e| format!("Failed to add backend column: {}", e))?;
}
Ok(())
}
}
fn parse_status(s: &str) -> MissionStatus {