Skip to content

Commit 1ebb78a

Browse files
authored
refactor(db): consolidate migrations + fix aionrs session resume (#233)
* fix(db): add migration 014 to backfill legacy data from pre-split era Two issues found when resuming historical conversations after migrating from the old monolithic architecture: 1. conversations.model stored the full provider object with model as an array, but the Rust backend expects a flat {provider_id, model, use_model} format. This caused warmup 500 errors for 221 conversations. 2. acp_session table was empty - the old TypeScript backend stored session IDs only in conversations.extra. Without acp_session rows the backend cannot resume sessions and treats them as new conversations. * refactor(db): consolidate 14 migrations into 2-file baseline Since the backend has not shipped yet, squash all intermediate dev-era migrations (001-014) into a clean two-file structure: - 001_initial_schema.sql: complete table/index definitions - 002_legacy_data_normalize.sql: data transformations for databases copied from the pre-split aionui.db (camelCase→snake_case in conversations.extra and teams.agents, model array→flat format, acp_session backfill from conversations.extra) Also move ensure_schema_columns() to run BEFORE sqlx migrations so that columns added for legacy DBs (pinned, agents_version, etc.) are available when 002 executes. * fix(aionrs): fallback to workspace session dir for legacy resume Old architecture stored aionrs sessions inside each workspace's .aionrs/sessions/ directory. New architecture uses a centralized data_dir/aionrs-sessions/ path. When the centralized lookup fails, fall back to the workspace-local path so legacy conversations can still resume. Once resumed, subsequent saves go to the new central directory automatically. * fix(test): update agent_metadata tests for renamed seed data Tests referenced old names (Claude, OpenClaw Gateway) that were updated to (Claude Code, OpenClaw) in the consolidated migration. * style: apply rustfmt to agent_metadata tests --------- Co-authored-by: zynx <>
1 parent 4d2f63e commit 1ebb78a

17 files changed

Lines changed: 547 additions & 811 deletions

crates/aionui-ai-agent/src/factory/aionrs.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,29 @@ pub(super) async fn build(
108108
);
109109
Some(session)
110110
}
111-
Err(e) => {
112-
debug!(
113-
conversation_id = %ctx.conversation_id,
114-
error = %e,
115-
"No existing aionrs session found, starting fresh"
116-
);
117-
None
111+
Err(_) => {
112+
// Fallback: old architecture stored sessions inside the workspace
113+
let legacy_dir = std::path::Path::new(&ctx.workspace).join(".aionrs/sessions");
114+
let legacy_mgr = SessionManager::new(legacy_dir.clone(), 100);
115+
match legacy_mgr.load(&ctx.conversation_id) {
116+
Ok(session) => {
117+
info!(
118+
conversation_id = %ctx.conversation_id,
119+
session_id = %session.id,
120+
message_count = session.messages.len(),
121+
"Loaded legacy aionrs session from workspace"
122+
);
123+
Some(session)
124+
}
125+
Err(e) => {
126+
debug!(
127+
conversation_id = %ctx.conversation_id,
128+
error = %e,
129+
"No existing aionrs session found, starting fresh"
130+
);
131+
None
132+
}
133+
}
118134
}
119135
}
120136
};

crates/aionui-db/migrations/001_initial_schema.sql

Lines changed: 335 additions & 115 deletions
Large diffs are not rendered by default.

crates/aionui-db/migrations/002_backend_extensions.sql

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
-- Migration 002: Normalize legacy data from the pre-split TypeScript era
2+
--
3+
-- When aionui-backend.db is created by copying the Electron-managed aionui.db,
4+
-- the data still uses the old formats (camelCase JSON keys, array model fields,
5+
-- empty acp_session table). This migration brings all legacy data to the format
6+
-- expected by the Rust backend.
7+
--
8+
-- Safe to run on fresh databases (all statements are conditional / idempotent).
9+
10+
------------------------------------------------------------------------
11+
-- Part A: Normalize conversations.extra JSON keys (camelCase → snake_case)
12+
--
13+
-- NOTE: Missing columns (pinned, pinned_at, agents_version, etc.) are
14+
-- handled by ensure_schema_columns() in database.rs which runs before
15+
-- migrations. This migration only performs data transformations.
16+
------------------------------------------------------------------------
17+
18+
UPDATE conversations
19+
SET extra = json_set(json_remove(extra, '$.agentName'), '$.agent_name', json_extract(extra, '$.agentName'))
20+
WHERE json_extract(extra, '$.agentName') IS NOT NULL
21+
AND json_extract(extra, '$.agent_name') IS NULL;
22+
23+
UPDATE conversations
24+
SET extra = json_set(json_remove(extra, '$.cliPath'), '$.cli_path', json_extract(extra, '$.cliPath'))
25+
WHERE json_extract(extra, '$.cliPath') IS NOT NULL
26+
AND json_extract(extra, '$.cli_path') IS NULL;
27+
28+
UPDATE conversations
29+
SET extra = json_set(json_remove(extra, '$.currentModelId'), '$.current_model_id', json_extract(extra, '$.currentModelId'))
30+
WHERE json_extract(extra, '$.currentModelId') IS NOT NULL
31+
AND json_extract(extra, '$.current_model_id') IS NULL;
32+
33+
UPDATE conversations
34+
SET extra = json_set(json_remove(extra, '$.sessionMode'), '$.session_mode', json_extract(extra, '$.sessionMode'))
35+
WHERE json_extract(extra, '$.sessionMode') IS NOT NULL
36+
AND json_extract(extra, '$.session_mode') IS NULL;
37+
38+
UPDATE conversations
39+
SET extra = json_set(json_remove(extra, '$.customWorkspace'), '$.custom_workspace', json_extract(extra, '$.customWorkspace'))
40+
WHERE json_extract(extra, '$.customWorkspace') IS NOT NULL
41+
AND json_extract(extra, '$.custom_workspace') IS NULL;
42+
43+
UPDATE conversations
44+
SET extra = json_set(json_remove(extra, '$.defaultFiles'), '$.default_files', json_extract(extra, '$.defaultFiles'))
45+
WHERE json_extract(extra, '$.defaultFiles') IS NOT NULL
46+
AND json_extract(extra, '$.default_files') IS NULL;
47+
48+
UPDATE conversations
49+
SET extra = json_set(json_remove(extra, '$.acpSessionConversationId'), '$.acp_session_conversation_id', json_extract(extra, '$.acpSessionConversationId'))
50+
WHERE json_extract(extra, '$.acpSessionConversationId') IS NOT NULL;
51+
52+
UPDATE conversations
53+
SET extra = json_set(json_remove(extra, '$.acpSessionId'), '$.acp_session_id', json_extract(extra, '$.acpSessionId'))
54+
WHERE json_extract(extra, '$.acpSessionId') IS NOT NULL;
55+
56+
UPDATE conversations
57+
SET extra = json_set(json_remove(extra, '$.acpSessionUpdatedAt'), '$.acp_session_updated_at', json_extract(extra, '$.acpSessionUpdatedAt'))
58+
WHERE json_extract(extra, '$.acpSessionUpdatedAt') IS NOT NULL;
59+
60+
UPDATE conversations
61+
SET extra = json_set(extra, '$.team_id', json_extract(extra, '$.teamId'))
62+
WHERE json_extract(extra, '$.teamId') IS NOT NULL
63+
AND json_extract(extra, '$.team_id') IS NULL;
64+
65+
UPDATE conversations
66+
SET extra = json_set(json_remove(extra, '$.customAgentId'), '$.custom_agent_id', json_extract(extra, '$.customAgentId'))
67+
WHERE json_extract(extra, '$.customAgentId') IS NOT NULL
68+
AND json_extract(extra, '$.custom_agent_id') IS NULL;
69+
70+
-- Clean up stale runtime caches
71+
UPDATE conversations
72+
SET extra = json_remove(extra, '$.cachedConfigOptions', '$.loadedSkills', '$.lastContextLimit', '$.lastTokenUsage')
73+
WHERE json_extract(extra, '$.cachedConfigOptions') IS NOT NULL
74+
OR json_extract(extra, '$.loadedSkills') IS NOT NULL;
75+
76+
-- Rename legacy teamMcpStdioConfig → legacy_team_mcp_stdio_config
77+
UPDATE conversations
78+
SET extra = json_set(
79+
json_remove(extra, '$.teamMcpStdioConfig'),
80+
'$.legacy_team_mcp_stdio_config', json_extract(extra, '$.teamMcpStdioConfig')
81+
)
82+
WHERE json_extract(extra, '$.teamMcpStdioConfig') IS NOT NULL
83+
AND json_extract(extra, '$.legacy_team_mcp_stdio_config') IS NULL;
84+
85+
------------------------------------------------------------------------
86+
-- Part B: Normalize conversations.model from legacy provider format
87+
--
88+
-- Legacy: {"id":"xxx", "model":["gpt-5.2","gpt-4o"], "useModel":"gpt-5.2", ...}
89+
-- Target: {"provider_id":"xxx", "model":"gpt-5.2", "use_model":null}
90+
------------------------------------------------------------------------
91+
92+
UPDATE conversations
93+
SET model = json_object(
94+
'provider_id', json_extract(model, '$.id'),
95+
'model', json_extract(model, '$.useModel'),
96+
'use_model', NULL
97+
)
98+
WHERE model IS NOT NULL
99+
AND json_valid(model)
100+
AND json_type(model, '$.model') = 'array'
101+
AND json_extract(model, '$.useModel') IS NOT NULL;
102+
103+
------------------------------------------------------------------------
104+
-- Part C: Normalize teams.agents JSON (camelCase → snake_case)
105+
--
106+
-- Only runs on teams with agents_version = '1.0.0' (pre-normalization).
107+
-- After conversion sets agents_version = '1.0.1'.
108+
------------------------------------------------------------------------
109+
110+
UPDATE teams
111+
SET agents = (
112+
SELECT json_group_array(
113+
json_object(
114+
'slot_id', json_extract(value, '$.slotId'),
115+
'name', COALESCE(json_extract(value, '$.agentName'), json_extract(value, '$.name'), ''),
116+
'role', CASE
117+
WHEN COALESCE(json_extract(value, '$.role'), '') IN ('lead', 'leader') THEN 'lead'
118+
ELSE 'teammate'
119+
END,
120+
'conversation_id', COALESCE(json_extract(value, '$.conversationId'), json_extract(value, '$.conversation_id'), ''),
121+
'backend', COALESCE(json_extract(value, '$.agentType'), json_extract(value, '$.backend'), ''),
122+
'model', COALESCE(json_extract(value, '$.model'), ''),
123+
'status', COALESCE(json_extract(value, '$.status'), 'pending'),
124+
'conversation_type', COALESCE(json_extract(value, '$.conversationType'), json_extract(value, '$.conversation_type'), ''),
125+
'cli_path', json_extract(value, '$.cliPath'),
126+
'custom_agent_id', json_extract(value, '$.customAgentId')
127+
)
128+
)
129+
FROM json_each(teams.agents)
130+
),
131+
agents_version = '1.0.1'
132+
WHERE agents_version = '1.0.0'
133+
AND json_valid(agents)
134+
AND json_array_length(agents) > 0
135+
AND json_extract(agents, '$[0].slotId') IS NOT NULL;
136+
137+
-- Teams with empty agents arrays also get marked as normalized
138+
UPDATE teams
139+
SET agents_version = '1.0.1'
140+
WHERE agents_version = '1.0.0'
141+
AND (agents = '[]' OR json_array_length(agents) = 0);
142+
143+
------------------------------------------------------------------------
144+
-- Part D: Backfill acp_session rows from conversations.extra
145+
------------------------------------------------------------------------
146+
147+
INSERT OR IGNORE INTO acp_session (
148+
conversation_id,
149+
agent_backend,
150+
agent_source,
151+
agent_id,
152+
session_id,
153+
session_status,
154+
session_config
155+
)
156+
SELECT
157+
c.id,
158+
COALESCE(json_extract(c.extra, '$.backend'), ''),
159+
'builtin',
160+
'',
161+
json_extract(c.extra, '$.acp_session_id'),
162+
'idle',
163+
'{}'
164+
FROM conversations c
165+
WHERE c.type = 'acp'
166+
AND json_extract(c.extra, '$.acp_session_id') IS NOT NULL
167+
AND c.id NOT IN (SELECT conversation_id FROM acp_session);

crates/aionui-db/migrations/003_assistants.sql

Lines changed: 0 additions & 32 deletions
This file was deleted.

crates/aionui-db/migrations/004_conversation_artifacts.sql

Lines changed: 0 additions & 23 deletions
This file was deleted.

crates/aionui-db/migrations/005_remove_session_agent_type_check.sql

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)