docs/backend-migration/modules/provider-config.md
Tracks the backend/frontend migration of model provider configuration
off the legacy local model.config key and onto /api/providers/*.
specs/2026-04-24-model-config-backend-migration-design.mdspecs/2026-04-24-model-config-frontend-migration-design.mdplans/2026-04-24-model-config-migration-plan.mdhandoffs/coordinator-model-config-2026-04-24.mde2e-reports/2026-04-24-model-config-migration.mdTeam: coordinator + backend-dev + frontend-dev + frontend-tester.
Work shipped:
aionui-backend feat/model-sync-be → feat/builtin-skills
@ d08255e):
POST /api/providers/fetch-models endpoint.client_preferences.model.config JSON into the providers
table (631 lines + 16 tests, idempotent, non-fatal failure).AionUi feat/model-sync-fe → feat/backend-migration-coordinator
@ dc8b11754): IProvider model → models + lastCheck → last_check; ipcBridge.mode rewritten to single-provider CRUD;
model.config removed from ConfigKeyMap + legacy migration; ~30
consumer sites rewired; 29 new Vitest tests.Outcome verification:
GET /api/settings/client no longer leaks IProvider array.Symptom: Coordinator initially ruled "strict UUID validation" for provider id, then within 3 minutes flipped to "lenient (1..=128 chars
uuid()
returns 8-char hex by default. Spec fix 2a63132 pushed. Coordinator
sent a new arbitration message.Backend-dev then spent ~4 rounds re-confirming whether the lenient impl should stay or revert to strict. His inbox held BOTH arbitration messages. Without an explicit "IGNORE PREVIOUS" marker he couldn't tell which was authoritative just from chronological order.
Fix (playbook rule): When flipping a decision mid-stream, always:
**SUPERSEDES earlier "X" arbitration. That directive is void.** — explicit, not just implied by
timestamp order.Applies to both teammate and user-flip scenarios.
Symptom: frontend-tester went silent at 16:38 after writing 4 test files (~949 lines) — messages unread, no commits, no replies for 11+ minutes.
Diagnosis: standard zombie criteria met (playbook top), executed autonomous replacement.
Outcome: replacement agent ran each of the 4 files — ALL GREEN. Zombie had actually completed the writing work but died between "files saved" and "commit + report". Replacement picked up the dirty worktree and proceeded cleanly, finishing T2.5 in one extra session.
Playbook addition: the replacement prompt should explicitly
instruct "prior agent's dirty worktree files may be complete, may be
half-done, or may be wrong — your first action is to MEASURE before
deciding to delete or continue". Prevents the instinct to
git stash drop and restart, which in this case would have wasted
the zombie's ~30 min of work.
Symptom: Initial pilot closed cleanly on all gates. User booted
backend against their real dev DB, reported /api/providers empty.
Root cause: 4 legacy providers still sitting in client_preferences. model.config, untouched. My scope decision at pilot start: "user
said no migration → skip it entirely". Wrong interpretation.
Why I got it wrong: "pre-launch" ambiguous between (a) "no production rollout concerns, no need to handle v1-to-v2 drift across N user installs", (b) "my own dev state is disposable, greenfield is fine". User meant (a). I assumed (b). The gap costs a full re-pilot day (T4 added after closure, plus re-merge, plus re-handoff update).
Fix (playbook rule): When scoping any "pre-launch, no migration" task, ALWAYS verify:
sqlite3 <user-data-dir>/aionui.db "SELECT COUNT(*) FROM <relevant_table>;" — is the relevant table actually empty in
user's dev env?This turns "no migration needed" into a verifiable claim instead of a coordinator inference.
Symptom: First attempt to end-to-end verify T4 on a copy of
user's real DB showed migration didn't fire. I had cp to
<sandbox>/aionui/aionui.db (matching user disk layout) and
launched backend with --data-dir <sandbox> — backend then
opened <sandbox>/aionui.db (a different DB, empty) and correctly
found no migration to do.
Frontend actually passes <userData>/aionui as --data-dir via
getDataPath() in src/process/utils/utils.ts. Backend opens
<data-dir>/aionui.db. So user data lives at
<userData>/aionui/aionui.db and the correct sandbox is
<sandbox>/aionui.db directly under --data-dir.
Fix: When building a sandbox for real-DB smoke testing, match
the call site's --data-dir construction, not the physical disk
layout. Source the exact argument line from the consumer
(here: lifecycleManager.ts:21, ['--port', ..., '--data-dir', config.dbPath] with dbPath = getDataPath() = <userData>/aionui).
Symptom: coordinator suggested running tests/e2e/features/settings/skills/core-ui.e2e.ts
as a "light settings smoke" after model-selection scenario. User
flagged: that suite is Skills Hub Settings, has zero overlap with
Model Settings. Pure coverage-irrelevant burn.
Fix (playbook rule): when recommending smoke suites at closure, grep for the migration's touch points FIRST:
grep -l <migrated-module-name> across tests/e2e/