src/main/data/migration/v2/migrators/README-KnowledgeMigrator.md
KnowledgeMigrator migrates legacy knowledge data from Redux + Dexie exports into the new SQLite schema.
| Data | Source | File/Path |
|---|---|---|
| Knowledge bases + lightweight items | Redux knowledge.bases | ReduxStateReader.getCategory('knowledge') |
| Full note content | Dexie knowledge_notes | knowledge_notes.json |
| File metadata fallback | Dexie files | files.json |
| Legacy vector databases | Filesystem | ctx.paths.knowledgeBaseDir/<sanitizedBaseId> (via MigrationPaths) |
Note: The legacy vector DB path comes from
ctx.paths.knowledgeBaseDir, which is pre-computed byMigrationPathsfrom the resolved v1 userData directory. The base id is sanitized withsanitizeFilename(baseId, '_'). Do NOT callapp.getPath('userData')directly — seemigration/v2/README.mdPath Safety section.
knowledge_baseknowledge_itemBase metadata migration
embeddingModelId and rerankModelId.user_model rows.embeddingModelId = null, status = failed, and error = missing_embedding_model.error = missing_embedding_model is the current shared KnowledgeBaseErrorCode member for recoverable base-level embedding model loss.fileProcessorId.Unified item payload migration
content is transformed into the new knowledge_item.data union payload by item type.file, url, note, and directory.sitemap items with valid string content are migrated as ordinary url items.knowledge_item as a flat item list with optional groupId.groupId = null by design.directory is a container/source declaration in knowledge_item; its own container-level vectors are handled by KnowledgeVectorMigrator as non-indexable and are not written to the V2 vector store.Note content source priority
knowledge_notes content.content when note export is missing.Dexie lookup loading strategy
knowledge_notes and files are scanned via streaming readers.Processing status normalization
processingStatus is treated as runtime-only and not trusted for migration.uniqueId:
uniqueId present and non-empty -> completedidleVector dimension dependency
knowledge_base.dimensions value.vectors.vector blob length.KnowledgeVectorMigrator.failed; valid legacy dimensions are kept, otherwise dimensions is null.| Source (Legacy base) | Target (knowledge_base) | Notes |
|---|---|---|
id | id | Direct copy |
name | name | Direct copy |
| no legacy grouping field | groupId | V1 knowledge bases do not carry group metadata; migrate as null |
dimensions | dimensions | Completed bases use legacy vector DB blob length (length(vector)/4); failed bases keep valid legacy dimensions or null |
model | embeddingModelId / status / error | Converted to provider::modelId, then resolved against user_model; missing/dangling references produce a failed recoverable base |
rerankModel | rerankModelId | Optional, converted to provider::modelId, then resolved against user_model; dangling references are cleared |
preprocessProvider.provider.id | fileProcessorId | Optional |
chunkSize | chunkSize | Copied when positive integer; otherwise normalized to the default chunk size |
chunkOverlap | chunkOverlap | Copied when non-negative integer and smaller than chunkSize; otherwise normalized to the default overlap for the resolved chunk size |
threshold | threshold | Copied when within [0, 1]; otherwise cleared |
documentCount | documentCount | Copied when positive; otherwise cleared |
created_at | createdAt | Timestamp conversion |
updated_at | updatedAt | Timestamp conversion |
| Source (Legacy item) | Target (knowledge_item) | Notes |
|---|---|---|
id | id | Direct copy |
base owner id | baseId | From parent base |
| no legacy grouping field | groupId | V1 exports are flat, so migrated items are inserted without grouping metadata (null) — except the synthesized children of an expanded directory, which point at their container |
type | type | Supported target types: file/url/note/directory. Legacy sitemap maps to url. |
content + Dexie lookups | data | Type-specific transform |
uniqueId | status | uniqueId non-empty => completed, otherwise idle |
processingError | error | Direct copy |
created_at | createdAt | Timestamp conversion |
updated_at | updatedAt | Timestamp conversion |
video items are skipped.memory items are skipped.prepare.directory items are migrated into knowledge_item as container/source declarations when their legacy payload is valid.sitemap items are migrated into knowledge_item as url items when their legacy payload is valid.knowledge_item ids for every expanded directory child document, but it does record one loader source string per embedded file. expandLegacyDirectoryItem therefore synthesizes one file child per distinct loader source so the v1 vectors can be re-attributed instead of dropped.raw/ prefix (docs, docs_1, …) and each child takes <prefix>/<its path relative to the folder>. The subtree is derived purely from the v1 source strings — both separators, case-folded segment comparison, per-segment sanitizeFilename — with no filesystem access, so the same v1 export migrates identically on macOS and Windows. A source recorded outside the container's folder path falls back to its filename alone and is counted into one aggregated warning per container.foldPathSegment key (NFC + lower-case), not by literal name, while the emitted prefix keeps its original spelling. raw/docs and raw/Docs are one directory on Windows and default macOS volumes, so two v1 folders differing only in case (or in Unicode composition) must not both claim it — otherwise deleting or re-indexing either container runs removeDir(raw/<prefix>) over the other's bytes and leaves its rows and index entries behind. .Cherry is folded against CHERRY_META_DIR for the same reason, even though assertSafeKnowledgeRelativePath only rejects the exact lower-case spelling.
collectReservedTopLevelNames / chooseDirectoryPathPrefix compare literally, so a container re-index can re-pick a case-colliding prefix, and reserveImportedFileRelativePath can hand a copied file a name that case-collides with a directory prefix. Both predate the migration and affect natively added directories today.raw/<prefix> for real, converging the migrated shape onto the native one. The prefix is normally re-picked identically — chooseDirectoryPathPrefix excludes the container itself and derives from the same basename — so paths and display names do not churn. Two cases do change paths on that reindex:
_N at migration time, when whatever forced the suffix has since been deleted: the reindex reclaims the shorter name.sanitizeFilename rewrote. The migration sanitizes every segment; the native expansion does not sanitize at all, because it walks a folder that exists on this machine — chooseDirectoryPathPrefix takes its path.basename and expandDirectoryNode reuses the treePath it just read — so those names are legal here by construction and sanitizing would only misname real files. The migration reads v1 strings that may have been recorded on another OS, cannot verify them against a local file, and is the sole guarantor that the emitted path passes assertSafeKnowledgeRelativePath — so it cannot skip the step. Consequence: a folder named a<b migrates to a_b and reverts to a<b when its container is reindexed on a POSIX host. Both spellings are valid v2 paths; only which one is in force changes.directory item are considered container-level vectors and are skipped by KnowledgeVectorMigrator with warnings.sitemap item are migrated as URL vectors because the item now maps to target type url.file, url, or note item id.The two phases split ownership of the base's raw/ namespace, and the order is not interchangeable:
prepare pins directory prefixes. A container's prefix is written into container.data.relativePath and immediately becomes the item row, the index-store material.relative_path, and the UI display name — none of which can be rewritten later. Prefixes are deduped against a per-base set (raw/ is per-base, so sharing one across bases would needlessly push the second base's docs to docs_1) seeded with CHERRY_META_DIR, because a v1 folder literally named .cherry would otherwise emit a relativePath that assertSafeKnowledgeRelativePath rejects on every read.execute names copied files, and yields. copyKnowledgeFilesForBase seeds its reservedPaths with .cherry plus every directory container's already-pinned prefix, so a v1 file named docs lands on docs_1 instead of squatting in raw/docs — where deleting or reindexing the container would removeDir it. Filenames are the ones that can move: reserveImportedFileRelativePath's _N suffix exists for exactly this.What forces this direction is not which phase can compute a name — prepare already knows the base's fileProcessorId, and it does touch the filesystem to resolve dimensions — but what a name is already load-bearing for. A prefix becomes a live item row, index material and display name the moment prepare writes it, so it can never move. A file's relativePath is committed nowhere until the copy loop runs, so it can. Moving the filename reservation into prepare would not change that; it would only split the copy loop's dedup state across two phases.
Directory children are skipped by the copy loop and never enter reservedPaths: they carry no bytes, their paths are already final, and they cannot collide with a copied file because every child path has a <prefix>/ segment while every copied filename is a single segment.
dimensions is required only for completed bases; failed migrated bases may have dimensions = null.dimensions field is not treated as the migration source of truth.dimensions is resolved from legacy vector DB content by inspecting:
vectors tablelength(vector)/4)preparenull, status is set to failed, and error is set to missing_embedding_model with a warning. That error value is a shared KnowledgeBaseErrorCode, not a free-form string. It does not require legacy vector DB inspection; valid legacy dimensions are preserved and invalid or missing legacy dimensions are stored as null.chunkSize, chunkOverlap, threshold, documentCount) is migrated on a best-effort basis:
chunkSize / chunkOverlap values are replaced with defaultsthreshold / documentCount are clearedknowledge_item flat and uses optional groupId for grouping queries.null.groupId after migration.(baseId, groupId) -> (baseId, id).A common recoverable case is a legacy knowledge base whose embedding model id exists in Redux but not in the V2 user_model table. For example, Redux may contain ollama::dengcao/Qwen3-Embedding-0.6B:Q8_0 while no matching migrated user model row exists.
The migrator handles this as a recoverable failed base:
embeddingModelId = null
status = failed
error = missing_embedding_model
The base and its knowledge_item rows are preserved. KnowledgeVectorMigrator skips vectors for this base because the embedding model contract cannot be verified.
User recovery is handled by runtime restore, not by mutating the failed base in place:
knowledge:restore-base
-> create a new knowledge base with the source base config and selected embedding model
-> copy source root items only
-> run the normal createBase + addItems indexing flow
The original failed base remains available after restore so the UI can let the user confirm success before deleting it.
sourceCounttargetCountskippedCountknowledge_item rows without valid knowledge_base.