Back to Cherry Studio

AssistantMigrator

src/main/data/migration/v2/migrators/README-AssistantMigrator.md

2.0.08.6 KB
Original Source

AssistantMigrator

The AssistantMigrator migrates assistants and presets from the v1 Redux state into the v2 assistant table (plus group, assistant_mcp_server, and assistant_knowledge_base). Legacy assistant tags were the v1 grouping mechanism, so they become group rows with entityType='assistant' and assistant.groupId references rather than v2 tags.

Data Sources

DataSourceNotes
User assistantsRedux state.assistants.assistants[]Includes the v1 initial-state copy of the default assistant (id=default)
Saved presetsRedux state.assistants.presets[]
Default assistant slotRedux state.assistants.defaultAssistantStandalone slot, id=default — has its own update path (updateDefaultAssistant) and can drift from assistants[0]
Assistant group orderRedux state.assistants.tagsOrder[]Orders the migrated assistant groups; stale names without a grouped assistant are ignored

Why the v1 Slice Has Two Default Slots

The v1 slice's initialState seeds both state.assistants.defaultAssistant and state.assistants[0] from the same getDefaultAssistant() factory (id=default). Reducers then update one or the other independently:

  • updateDefaultAssistant writes only to the slot.
  • updateAssistant / updateAssistantSettings / addTopic write only to assistants[].

In practice, real users typically have both slots populated with overlapping but non-equivalent data on id=default. The migrator must look at both and reconcile them, otherwise customizations on whichever slot wasn't picked are silently lost.

Same-id Merge Strategy

When two sources carry the same id, mergeOldAssistants(primary, secondary) produces a single merged assistant. Duplicates are NOT skipped.

Push order is assistants[]presets[]defaultAssistant, so assistants[0] is the primary in the common id=default collision (it gets the live edits from the assistants page), and defaultAssistant only fills in fields the live copy left empty.

"Non-empty" Rules

A field on primary "wins" only when it is present. Otherwise secondary's value is used.

TypeTreated as empty (falls through to secondary)
stringundefined, null, ''
Arrayundefined, null, []
Plain objectundefined, null, {} (e.g. default-seeded defaultModel: {})
Booleanundefined, null only — false is a real choice
Object (settings root)undefined, null (the settings root itself; nested keys follow the same rules)

The empty-array rule prevents a default-empty mcpServers: [] on assistants[0] from clobbering a populated mcpServers: [s1] on defaultAssistant.

Settings Shallow Merge

primary.settings and secondary.settings are shallow-merged per key with the same "non-empty wins" rule. Nested objects (e.g. defaultModel, customParameters) are not deep-merged — the first-non-empty top-level reference wins.

Unenumerated Fields

The merged object is built as { ...secondary, ...primary, /* explicit overrides */ }, so any field not listed in OldAssistant (e.g. fields from older v1 versions) survives the merge: secondary provides a baseline, primary overrides on overlap.

Data Quality Handling

IssueDetectionHandling
Missing/invalid id!id or typeof id !== 'string'Skip source, log warning
Same id across sourcessourceById.has(id)Merge field-by-field (see above); silent at info-log level — v1's initialState seeds id='default' in both assistants[0] and defaultAssistant, so this fires on essentially every real-user migration
Legacy id 'default'rawId === 'default'Remap to a fresh UUID before merge / insert (see "Legacy default-assistant remap" below)
Transform failuretransformAssistant() throwsSkip merged source, log warning
Legacy group name exceeds the current 64-character edit limitFirst normalized legacy tag is non-emptyPreserve it exactly; the persisted Group entity accepts v1-compatible names
All sources skippedtotalRawSources > 0 && skippedCount > 0 && preparedResults.length === 0Fail prepare phase
Dangling model refuserModelTable lookup missDrop modelId (set to null), log warning
Dangling MCP server refmcpServerIdMapping lookup missDrop the junction row, log warning
Dangling knowledge base refknowledgeBaseTable lookup missDrop the assistant_knowledge_base row, log warning
Missing mcpServerIdMapping while assistants reference MCP serverssharedData.get('mcpServerIdMapping') === undefinedThrow — McpServerMigrator must run before this one

Legacy tag → group migration

The v1 UI treated assistant tags as named, ordered groups. Each assistant had at most one group, stored as either tags: [name] or tags: [], while tagsOrder stored the group order. During preparation the migrator normalizes that optional name and orders the unique used names by tagsOrder followed by first appearance. Because v1 did not impose the current 64-character edit limit, longer legacy names are preserved rather than dropping the assistant's grouping.

During execution it inserts one group row per used name with entityType='assistant', then writes the generated group ID to each matching assistant.groupId. Assistants without a tag remain ungrouped (groupId = NULL). The group and assistant rows are inserted in the same transaction.

Before a retry, MigrationEngine clears group plus the obsolete entity_tag / tag targets used by the previous implementation, so a failed older attempt cannot leave the same v1 grouping data classified both ways.

Legacy default-assistant remap

v2 has no system-reserved 'default' assistant row. New installs get the managed default assistant through DefaultAssistantSeeder; upgraded profiles get only user-created/migrated rows from this migrator.

To preserve user customizations made to v1's default assistant, recordSource() rewrites the legacy literal 'default' to a freshly generated UUID before inserting into sourceById. Both v1 sources for that id (assistants[0] and the standalone defaultAssistant slot) collide on the same UUID and merge under the standard primary-wins contract. The remap ('default' → UUID) is held on the migrator instance for the duration of prepare/execute and exposed through ctx.sharedData so ChatMigrator can rewrite any topic.assistantId === 'default' to the same UUID rather than orphaning the topic.

If v1 had no 'default' source at all (no assistants[0]/defaultAssistant), no remap entry is created and any topic that referenced the legacy literal becomes a true orphan in ChatMigrator — written as assistantId = NULL.

Downstream Hand-off

AssistantMigrator.execute() writes two entries to ctx.sharedData:

  • 'assistantIds'Set<string> of migrated assistant IDs (FK whitelist for ChatMigrator). Contains exactly the assistant.id values inserted into the table; v2 has no synthetic 'default' so the literal is absent.
  • 'legacyAssistantIdRemap'Map<string, string> of v1 → v2 id rewrites. Currently only used for 'default' → UUID (see above), but the map shape is generic for future legacy-id translations.

ChatMigrator.prepare() and prepareTopicData() consume both: the remap rewrites topic.assistantId references, then the FK whitelist validates the result.

Order-Key Backfill

Legacy Redux assistants have no stable per-row fractional key, so transformAssistant() omits orderKey. AssistantMigrator.execute() stamps the prepared assistant rows with assignOrderKeysInSequence() immediately before insert, preserving the merged source order used by recordSource().

Legacy group names are also stamped with assignOrderKeysInSequence() in tagsOrder order before insertion into group.

Dropped Relationship Rows

The migrator keeps assistant rows even when optional relationship targets are missing, but drops junction rows that would violate foreign keys:

  • assistant_mcp_server rows whose legacy MCP server id was not remapped by McpServerMigrator.
  • assistant_knowledge_base rows whose knowledge base was deleted or skipped before this migrator inserts relationships.

Both cases are logged. The dropped relationship does not drop the assistant itself.

Implementation Files

  • AssistantMigrator.ts - Main migrator class (prepare / execute / validate)
  • mappings/AssistantMappings.ts - Pure transform functions, normalized legacy tag name, and OldAssistant type