siyuan-note/siyuan · error

encrypted document history has no matching notebook [%s]

Error message

encrypted document history has no matching notebook [%s]

What it means

For a ciphertext history file the kernel extracts histBoxID from the path and checks that a notebook with that ID actually exists (IsEncryptedBox). If no matching encrypted notebook exists, it errors 'encrypted document history has no matching notebook [%s]'. This prevents decrypting history without the owning notebook's context and keys.

Source

Thrown at kernel/model/history.go:188

	data, err := filelock.ReadFile(historyPath)
	if err != nil {
		logging.LogErrorf("read file [%s] failed: %s", historyPath, err)
		return
	}

	// 加密笔记本的历史是密文,按路径里的 boxID 解密后解析
	relPath := strings.TrimPrefix(filepath.ToSlash(historyPath), filepath.ToSlash(util.HistoryDir))
	relPath = strings.TrimPrefix(relPath, "/")
	pathParts := strings.SplitN(relPath, "/", 3)
	ciphertext := util.IsCiphertext(data)
	if ciphertext {
		if len(pathParts) < 3 || !ast.IsNodeIDPattern(pathParts[1]) {
			err = errors.New("encrypted document history is missing notebook context")
			return
		}
		histBoxID := pathParts[1]
		if !IsEncryptedBox(histBoxID) {
			err = fmt.Errorf("encrypted document history has no matching notebook [%s]", histBoxID)
			return
		}
		HoldBoxReadLock(histBoxID)
		defer ReleaseBoxReadLock(histBoxID)
		dek, dekErr := GetDEKIfUnlocked(histBoxID)
		if dekErr != nil {
			err = errors.New(Conf.Language(314))
			return
		}
		data, err = DecryptFile(histBoxID, pathParts[2], dek, data)
		if err != nil {
			logging.LogErrorf("decrypt history [%s] failed: %s", historyPath, err)
			return
		}
	} else if len(pathParts) >= 2 && IsEncryptedBox(pathParts[1]) {
		err = fmt.Errorf("encrypted notebook document history is plaintext [%s]", pathParts[1])
		return
	}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Recreate the encrypted notebook (or restore its .si folder) with the same notebook ID before opening the history entry
  2. Copy the history entry back into a workspace where the matching notebook exists
  3. If history is no longer needed, ignore the entry; it cannot be opened without the notebook's keys
Defensive patterns

Strategy: fallback

Validate before calling

// check notebook still exists before opening its history
const notebooks = (await fetchPost('/api/notebook/lsNotebooks', {})).notebooks;
if (!notebooks.some(n => n.id === boxIDFromHistoryPath)) return null;

Try / catch

try {
  return await fetchPost('/api/history/getDocHistoryContent', {historyPath});
} catch (e) {
  if (String(e.msg).includes('no matching notebook')) {
    return {content: null, reason: 'notebook deleted; history not openable'};
  } else throw e;
}

Prevention

When it happens

Trigger: Viewing history of an encrypted notebook that has since been deleted, or whose ID changed (notebook recreated, restored from partial backup, sync conflict), so pathParts[1] names a nonexistent notebook.

Common situations: After deleting an encrypted notebook but browsing old history entries; restoring data/ from backup without the .si notebook folder; notebook ID mismatch after manual copying between workspaces.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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