siyuan-note/siyuan · error

hpath refresh crossed an encrypted notebook boundary

Error message

hpath refresh crossed an encrypted notebook boundary

What it means

refreshHPathsTask resolves the task's root document first from the task's box, and for non-encrypted boxes falls back to the global block tree. If the global lookup finds the document in an encrypted notebook while the task was issued against a non-encrypted box (or vice versa context mismatch), it means the hpath refresh would move data across an encrypted/plain notebook boundary — which is forbidden because the two notebooks use different storage (encryption) formats.

Solutions

  1. Rebuild the block-tree index so the task's document resolves to the correct box
  2. Verify the notebook encryption status of both task.Box and the tree record's BoxID (IsEncryptedBox) and fix the mismatched flag
  3. Delete the stale hpath refresh task so it is not retried against the wrong notebook
  4. If the document was genuinely moved across notebooks, re-trigger the operation through the normal move API rather than the queued refresh
Defensive patterns

Strategy: validation

Validate before calling

if root := treenode.GetBlockTree(id); root != nil && model.IsEncryptedBox(root.BoxID) != model.IsEncryptedBox(taskBox) { /* rebuild index before refreshing */ }

Try / catch

ok, err := task.refresh(); if err != nil && err.Error() == "hpath refresh crossed an encrypted notebook boundary" { // rebuild the block-tree index and clear the stale task }

Prevention

When it happens

Trigger: A queued hpath refresh task whose task.Box does not match the block-tree record's BoxID, where the tree record's box is an encrypted notebook while the task box is not (root == nil for task.Box, fallback GetBlockTree finds an encrypted root).

Common situations: Block-tree index stale after a notebook was switched to/from encrypted; a task referencing a document that was moved between notebooks; index rebuild race leaving task.Box out of sync.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/dee4ddb828a95b71. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/hpath_refresh.go:407

			return
		}
		sql.ClearCache()
		util.BroadcastByType("main", "databaseIndexCommit", 0, "", map[string]any{"rootIDs": []string{task.ID}, "backlinkChanged": true, "backlinkFull": true})
		if !task.started.IsZero() && time.Since(task.started) > time.Second {
			logging.LogInfof("refreshed document hpaths [%s], docs [%d], batches [%d], elapsed [%dms], active [%dms]", key, len(task.docs), task.batches, time.Since(task.started).Milliseconds(), task.active.Milliseconds())
		}
	}
}

func refreshHPathsTask(task *hpathRefreshTask) (done bool, err error) {
	if task.started.IsZero() {
		task.started = time.Now()
	}
	root := treenode.GetBlockTreeInBox(task.ID, task.Box)
	if !IsEncryptedBox(task.Box) && (root == nil || root.BoxID != task.Box) {
		root = treenode.GetBlockTree(task.ID)
		if root != nil && IsEncryptedBox(root.BoxID) {
			return false, errors.New("hpath refresh crossed an encrypted notebook boundary")
		}
	}
	if root == nil {
		// 索引尚未恢复时不能丢弃磁盘上仍存在的文档任务。
		if _, statErr := os.Stat(filepath.Join(util.DataDir, task.Box, task.Path)); statErr == nil {
			return false, errors.New("hpath refresh document is not indexed")
		} else if !os.IsNotExist(statErr) {
			return false, statErr
		}
		return true, nil
	}
	scope := sha256.Sum256([]byte(root.BoxID + "\x00" + root.Path + "\x00" + root.HPath))
	if task.scope != scope {
		task.docs, task.index, task.covers = nil, 0, nil
		task.blockAfter, task.treeAfter = 0, 0
		task.scope = scope
	}
	if task.docs == nil {

View on GitHub (pinned to 9f775e8a12)