siyuan-note/siyuan · error

block swap notebook has changed

Error message

block swap notebook has changed

What it means

restoreBlockSwapFragments re-checks, for each stored fragment, that its tree still exists in the provided map and still belongs to the same notebook (box ID) recorded at swap time. This error means a fragment's tree is missing from the snapshot or was moved to another notebook, so the swap cannot be safely replayed or finalized without corrupting content.

Solutions

  1. Restore the deleted/moved document(s) to their original notebooks, then retry
  2. Refresh/reopen the documents so the in-memory trees match disk state before replaying
  3. Discard the stale undo entry; the swap state is no longer valid
Defensive patterns

Strategy: try-catch

Validate before calling

for (const f of fragments) {
  const tree = trees[f.rootID];
  if (!tree || tree.box !== f.boxID) throw new Error('fragment tree ' + f.rootID + ' missing or moved');
}

Try / catch

try { await replaySwap(tx); } catch (e) {
  if (e.message.includes('block swap notebook has changed')) { /* restore docs/notebooks, or discard stale undo */ }
}

Prevention

When it happens

Trigger: Replaying a block swap after a fragment tree was deleted, or after the tree's root was moved to a different notebook so tree.Box != fragment.boxID; passing a trees map that omits one of the fragment roots.

Common situations: Undoing a swap after deleting a involved document; a tree moved between notebooks by a concurrent operation or another device via sync; partial workspace restore.

Related errors


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

Appendix: source

Thrown at kernel/model/transaction_block_swap.go:227

		if !IsSameCryptoBoundary(orderedTrees[0].Box, tree.Box) {
			return errors.New("cannot replay block swap across encrypted notebook boundaries")
		}
	}
	tx.saveBlockSwapOriginalTrees(orderedTrees)
	if err := restoreBlockSwapFragments(from, to, trees); err != nil {
		return err
	}
	tx.finishBlockSwap(from, to, orderedTrees)
	operation.RetData = state.rootIDs
	return nil
}

func restoreBlockSwapFragments(from, to []blockSwapFragment, trees map[string]*parse.Tree) error {
	removing := map[string]bool{}
	for _, fragment := range from {
		tree := trees[fragment.rootID]
		if tree == nil || tree.Box != fragment.boxID {
			return errors.New("block swap notebook has changed")
		}
		node := treenode.GetNodeInTree(tree, fragment.node.ID)
		if node == nil || node.Parent != tree.Root || blockSwapFingerprint(node) != blockSwapFingerprint(fragment.node) {
			return fmt.Errorf("block changed since conversion: %s", fragment.node.ID)
		}
		previousID, nextID := "", ""
		for previous := node.Previous; previous != nil; previous = previous.Previous {
			if previous.IsBlock() && previous.ID != "" {
				previousID = previous.ID
				break
			}
		}
		for next := node.Next; next != nil; next = next.Next {
			if next.IsBlock() && next.ID != "" {
				nextID = next.ID
				break
			}
		}

View on GitHub (pinned to 9f775e8a12)