1986a15dd5
Every persistence layer in crates/tui/src/ already gates `schema_version > CURRENT_*` to reject newer-than-supported records (good — prevents silent truncation when an older binary tries to load a v3 file with v4 fields). What was missing: the **forward upgrade path** for older records. When we bump CURRENT_SESSION_SCHEMA_VERSION from 3 to 4 to add a field, every v3 session on disk would silently load with the new field's serde default — which is OK for additions but breaks catastrophically for renames or shape changes. This commit lays down the framework: **`crates/tui/src/schema_migration.rs`** — new module: - `SchemaMigration` trait. Each persistence domain implements it once with `CURRENT_VERSION`, `DOMAIN`, and an ordered `MIGRATIONS` list of `fn(&mut serde_json::Value) -> Result<(), MigrationError>` steps. Index `i` migrates from version `i+1` to `i+2`. - `SchemaMigration::migrate(value, from_version)` — runs every required step, stamping `value["schema_version"]` after each step so a partial failure leaves a known-state record rather than mixed. - `MigrationError` — typed error with from/to versions + reason. - `backup_before_migrate(path, domain)` — creates a `.bak` copy of the source file before mutation. Errors are warn-logged and ignored (continues because `write_atomic` is itself crash-safe). The `.bak` is left on disk as a manual recovery artifact — no automatic GC. **`schema_migration::registry`** — submodule that registers every existing persistence domain (session, offline_queue, runtime, task, automation, automation_run) at its current version with an empty MIGRATIONS list. No domain has shipped a schema bump yet, so today's behavior is a no-op. The next bump is now a 4-step recipe: 1. Write the `migrate_<domain>_v<N>_to_v<N+1>` step in this module. 2. Append it to `MIGRATIONS` and bump `CURRENT_VERSION`. 3. Wire `<Domain>Migration::migrate(...)` into the load function in the owning module. 4. Add a fixture-based integration test. Tests: 6 unit tests covering no-op, all-steps, partial migration, newer-than-current rejection, backup creation, and backup-failure robustness. Wiring into individual load sites (session_manager, runtime_threads, task_manager, automation_manager) is intentionally deferred until the first actual schema bump needs it — wiring without migrations would add code paths nothing exercises, and the framework is the part that needs to land before the next bump can ship safely. Closes #350. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>