src/main/data/migration/v2/README.md
This directory contains the v2 data migration implementation.
src/main/data/migration/v2/
├── core/ # MigrationEngine, MigrationContext, MigrationPaths
├── migrators/ # Domain-specific migrators
│ └── mappings/ # Mapping definitions
├── migrationDiagnosticBundle.ts # Migration diagnostic ZIP builder
├── utils/ # ReduxStateReader, DexieFileReader, JSONStreamReader, LegacyHomeConfigReader
├── window/ # IPC handlers, window manager
└── index.ts # Public exports
MigrationPaths (Strict Requirement)⚠️ WARNING: Not using predefined paths may cause user data loss.
v1 users may have configured a custom userData directory via
~/.cherrystudio/config/config.json. If migration code callsapp.getPath('userData')ornew Store()directly, on the first v2 launch it will read from the Electron default path instead of the user's actual data directory — causing migration to be silently skipped or to migrate empty data, making user data appear lost.
All migration code MUST use the pre-computed path constants from
MigrationPaths. NEVER call app.getPath() directly or construct
paths with path.join() from scratch inside migration code.
| Correct ✅ | Wrong ❌ |
|---|---|
ctx.paths.userData | app.getPath('userData') |
ctx.paths.databaseFile | path.join(app.getPath('userData'), 'Data', 'cherrystudio.sqlite') |
ctx.paths.legacyClaudeConfigDir | path.join(ctx.paths.userData, '.claude') |
ctx.paths.legacyClaudeProjectsDir | path.join(ctx.paths.userData, '.claude', 'projects') |
ctx.paths.claudeConfigDir | path.join(ctx.paths.userData, 'Data', 'Agents', '.claude') |
ctx.paths.claudeProjectsDir | path.join(ctx.paths.userData, 'Data', 'Agents', '.claude', 'projects') |
ctx.paths.knowledgeBaseDir | path.join(app.getPath('userData'), 'Data', 'KnowledgeBase') |
ctx.paths.legacyConfigFile | path.join(os.homedir(), '.cherrystudio', 'config', 'config.json') |
new Store({ cwd: ctx.paths.userData }) | new Store() |
MigrationPaths is resolved once at the migration gate entry by
resolveMigrationPaths() (including v1 legacy userData detection),
then passed through MigrationContext.paths to all migrators. If you
need a new path, add it to the MigrationPaths interface — do not
construct it inline.
Migration diagnostics read the application's own logger output, not v1/v2 migration source data. This change
records one narrow exception to the rule above: logs MUST resolve through application.getPath('app.logs')
and MUST NOT be added to MigrationPaths. No broader migration filesystem access rule is relaxed.
Only Renderer error/version-incompatible pages can request a bundle; there is no native preboot entry. A bundle contains minimal system information plus a stable snapshot for one log date: prefer the failure panel's mount-time local date, otherwise choose the latest eligible date, and never mix dates. If a complete snapshot cannot be formed, publish metadata only and disclose that result in the UI only when the destination can still be proven safe. If destination or source identity cannot be established, saving fails without replacing the existing file. Metadata excludes failure stacks, paths, and run/process fields. Logs may be sensitive and must not be shared publicly or outside Cherry Studio support.
Before the migration window is created, the gate validates the upgrade
path using core/versionPolicy.ts. This catches manual installs that
bypass the auto-updater's version filtering.
Required upgrade path: v1.old → v1.last (≥1.9.12) → v2.0.0 → v2.x
| Rule | Condition | Reason |
|---|---|---|
| no_version_log | Legacy data exists but version.log is missing | User never ran a v1 version with VersionService (embedded since v1.7) |
| v1_too_old | previousVersion < V1_REQUIRED_VERSION | Data not in final v1 form |
| v2_gateway_skipped | previousVersion < 2.0.0 && coerce(currentVersion) > 2.0.0 | Skipped the v2.0.0 migration gateway |
v2.0.0 pre-releases (alpha/beta/rc) are treated as before v2.0.0 in semver ordering. This means:
gt('2.0.0', '2.0.0') is false)completed after the first successful runThe version check reads paths.versionLogFile (resolved by
MigrationPaths), NOT VersionService's cached path. This is
critical for v1 users with custom userData directories — see the
Path Safety section above.
BaseMigrator in migrators/prepare, execute, and validategetAllMigrators() list in migrators/migratorRegistry.tsctx.paths for all filesystem paths — NEVER call app.getPath() directlyprepare(ctx): Dry-run checks, return countsexecute(ctx): Perform inserts, report progressvalidate(ctx): Verify counts and integrityAssistantMigrator also owns v1 assistant tag-group migration: it inserts group(entityType='assistant') rows and assigns their IDs to assistant.groupId in the same transaction.
The engine keeps foreign_keys = OFF for the entire migration: MigrationDbService sets the
pragma once after migrations run. better-sqlite3 keeps a single connection, so that pragma persists
for the whole migration with no per-transaction replay. Migrators must NOT toggle FK
themselves. Verify integrity with this.assertOwnedForeignKeys(ctx.db, [...]) at the end of
execute() (own, fully-resolved tables only — exclude cross-domain-deferred and shared polymorphic
tables); the engine runs a final whole-database foreign_key_check as backstop. See the
migration guide for details.