siyuan-note/siyuan · error · obsidianUserError
345
345
Error message
read Markdown [%s]: %w
What it means
newObsidianReadUserError wraps any error returned by readStableObsidianFile during analysis/staging. Language code 345 ('Unable to read Markdown file [%s]') for ordinary read failures, or 346 ('File [%s] changed after analysis…') when the cause is the errObsidianSourceChanged sentinel. readStableObsidianFile fails on os.Stat failure, on a detected size/mtime drift, or on os.ReadFile error.
Source
Thrown at kernel/model/import_obsidian.go:953
if err != nil {
return nil, err
}
after, err := os.Stat(file.AbsPath)
if err != nil {
return nil, err
}
if before.Size() != after.Size() || !before.ModTime().Equal(after.ModTime()) {
return nil, fmt.Errorf("%w while reading", errObsidianSourceChanged)
}
return data, nil
}
func newObsidianReadUserError(file *obsidianSourceFile, err error) error {
language := 345
if errors.Is(err, errObsidianSourceChanged) {
language = 346
}
return newObsidianUserError(language, file.RelPath, fmt.Errorf("read Markdown [%s]: %w", file.RelPath, err))
}
func validateObsidianReadableFile(file *obsidianSourceFile) error {
opened, err := os.Open(file.AbsPath)
if err != nil {
return err
}
defer opened.Close()
buffer := make([]byte, 1)
_, err = opened.Read(buffer)
if err != nil && !errors.Is(err, io.EOF) {
return err
}
return validateObsidianSourceMetadata(file)
}
func buildObsidianHeadingIndex(doc *obsidianDocPlan, tree *parse.Tree) {
stack := make([]string, 6)View on GitHub (pinned to 251596fc0d)
Solutions
- If the message indicates 'changed after analysis' (code 346): stop editing/syncing the vault, then re-run StartObsidianVaultAnalysis to get a fresh snapshot.
- Confirm the file still exists and is readable (ls + cat as the SiYuan user); restore from backup if it was deleted/quarantined.
- Pause or exclude the vault in cloud-sync clients and antivirus during import, then retry.
- For mtime-drift false positives on network filesystems, copy the vault to a local FS and import from there.
Example fix
// before: Obsidian app open and auto-saving while importing // -> readStableObsidianFile sees mtime change -> code 346 // after: close Obsidian, pause sync, then analyse StartObsidianVaultAnalysis(localCopyPath)
Defensive patterns
Strategy: retry
Validate before calling
// Ensure the vault is quiescent: no mtime changes during a probe read.
func vaultStable(root string) error {
return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() { return err }
i1, e := os.Stat(p); if e != nil { return e }
b, e := os.ReadFile(p); if e != nil { return e }
i2, e := os.Stat(p); if e != nil { return e }
if i1.Size() != i2.Size() || !i1.ModTime().Equal(i2.ModTime()) {
return fmt.Errorf("%s changed during read", p)
}
_ = b
return nil
})
} Type guard
null
Try / catch
// Retry on read errors; re-analyse on drift.
taskID, err := StartObsidianVaultAnalysis(path)
for attempt := 0; attempt < 3 && isTransientReadErr(err); attempt++ {
time.Sleep(backoff(attempt))
taskID, err = StartObsidianVaultAnalysis(path)
} Prevention
- Close Obsidian and pause sync before importing.
- Run the importer from a local copy on a stable filesystem.
- Disable auto-save / touch-on-open tools for the vault.
When it happens
Trigger: File deleted or permission revoked between scan and read; file modified mid-read (size/mtime differ between the pre-Stat, ReadFile, and post-Stat snapshots — the 'stable read' guard fires errObsidianSourceChanged); disk I/O error during ReadFile. The relative path is included so the user can locate it.
Common situations: User kept editing the vault in Obsidian during import; a sync client (iCloud/Dropbox) rewrote files mid-analysis; antivirus quarantined the file; file on a flaky external drive; clock skew making mtime comparisons unstable.
Related errors
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/a1f0b66ccf97b98e.
Report an issue: GitHub.