siyuan-note/siyuan · error

block changed since conversion

Error message

block changed since conversion: %s

What it means

Before restoring swapped fragments, the kernel fingerprints each stored node (blockSwapFingerprint) and compares it with the current node found in the tree, also verifying the node still exists and is still a direct child of the document root. This error means the target block has been edited, moved, or deleted since the block swap state was captured, so blindly restoring would overwrite newer user content.

Solutions

  1. Accept the newer content and drop the stale swap/undo state
  2. Revert the recent edit to the block so it matches the swap-time content, then retry undo/redo
  3. Re-sync the workspace so both devices have consistent block content before replaying
Defensive patterns

Strategy: try-catch

Validate before calling

const node = findNode(tree, fragmentNodeId);
if (!node || node.parentId !== tree.rootId || fingerprint(node) !== fragment.fingerprint) {
  throw new Error('block ' + fragmentNodeId + ' changed since conversion');
}

Type guard

function isUnchangedSwapNode(tree, frag) {
  const node = tree && frag && frag.node && frag.node.id ? findNode(tree, frag.node.id) : null;
  return node != null && node.parentId === tree.rootId && fingerprint(node) === fingerprint(frag.node);
}

Try / catch

try { await replaySwap(tx); } catch (e) {
  if (e.message.startsWith('block changed since conversion')) { /* keep newer content; drop stale undo state */ }
}

Prevention

When it happens

Trigger: Undoing/redoing a swap after the fragment block's text was edited; the block was moved out of the document root (Parent != tree.Root); the block ID no longer resolves in the tree (node == nil).

Common situations: Editing a paragraph then undoing an earlier block swap; sync merging changes from another device between the swap and the replay; plugins modifying blocks after the swap state snapshot.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at kernel/model/transaction_block_swap.go:231

	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
			}
		}
		if previousID != fragment.previousID || nextID != fragment.nextID {
			return fmt.Errorf("block position changed since conversion: %s", fragment.node.ID)
		}
		for _, id := range node.BlockIDs() {

View on GitHub (pinned to 9f775e8a12)