siyuan-note/siyuan · error

read encrypted notebook history conf [%s] failed: %w

Error message

read encrypted notebook history conf [%s] failed: %w

What it means

isEncryptedHistoryBoxDir reads .siyuan/conf.json via filelock.ReadFile; a failed read is wrapped as 'read encrypted notebook history conf [%s] failed'. The library throws it because the BoxConf determines whether the history box holds encrypted material — a read failure must abort rather than default to 'unencrypted', which could permit unsafe deletion of recovery material.

Source

Thrown at kernel/model/crypto.go:950

// 再 fallback 到 conf.json 的 Encrypted 标志。
func isEncryptedHistoryBoxDir(boxDir string) (bool, error) {
	siyuanDir := filepath.Join(boxDir, ".siyuan")
	backupPath := filepath.Join(siyuanDir, "notebook-crypto-backup.json")
	if _, err := os.Stat(backupPath); err == nil {
		return true, nil
	} else if !os.IsNotExist(err) {
		return false, fmt.Errorf("stat encrypted notebook history backup [%s] failed: %w", boxDir, err)
	}
	confPath := filepath.Join(siyuanDir, "conf.json")
	if _, err := os.Stat(confPath); err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, fmt.Errorf("stat encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	data, err := filelock.ReadFile(confPath)
	if err != nil {
		return false, fmt.Errorf("read encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	var boxConf conf.BoxConf
	if err = gulu.JSON.UnmarshalJSON(data, &boxConf); err != nil {
		return false, fmt.Errorf("parse encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	return boxConf.Encrypted, nil
}

// cachedDEKs 缓存已解锁加密笔记本的 DEK,按 boxID 索引。
// KEK 不全局缓存("严格每笔记本单独解锁"语义):UnlockBox 临时派生 KEK 解出 DEK 后即丢弃 KEK,
// 仅保留 per-box DEK 供后续读写加解密。
var (
	cachedDEKs     = map[string][]byte{}
	cachedDEKsLock sync.RWMutex
)

// boxLastAccess 记录每个加密笔记本最近一次真实用户交互或显式保活时间(unix 纳秒),供自动锁定 cron 使用。
// key: boxID, value: *atomic.Int64。UnlockBox 成功时初始化,Unmount 时清理。

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Close other SiYuan instances/scripts locking the workspace, then retry
  2. Verify conf.json still exists and is readable after the stat (fix the delete race by pausing history cleanup)
  3. Read the wrapped cause; if it is an I/O/mount error, repair the storage first
  4. Restore conf.json from backup if the file itself is unreadable or truncated

Example fix

// before: second kernel instance locks the workspace
pkill -f siyuan-kernel  # all instances
// after: keep exactly one instance owning workspace locks
systemctl --user stop siyuan-old; run new kernel; retry scan
Defensive patterns

Strategy: retry

Validate before calling

if _, err := os.Stat(confPath); err == nil {
    if f, err := os.Open(confPath); err != nil { return err } else { f.Close() }
}

Try / catch

enc, err := isEncryptedHistoryBoxDir(boxDir)
if err != nil && isLockOrTransient(err) {
    time.Sleep(100 * time.Millisecond)
    enc, err = isEncryptedHistoryBoxDir(boxDir) // retry once after releasing other instances
}

Prevention

When it happens

Trigger: Callers of isEncryptedHistoryBoxDir hitting read errors on conf.json: the file is locked exclusively by another process, permission denied at open time (stat succeeded, open fails), or filelock failing on the underlying file (removed between stat and read, or filesystem-level I/O error).

Common situations: Two kernel instances (or a kernel plus a tooling script) running on the same workspace; the file deleted by history pruning between the stat and read; disk errors or a forcibly-unmounted network volume.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/716f6d09af9717d4. Report an issue: GitHub.