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
- Accept the newer content and drop the stale swap/undo state
- Revert the recent edit to the block so it matches the swap-time content, then retry undo/redo
- 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
- Avoid editing blocks between a swap and its undo/redo
- Complete pending swap replays before syncing changes from other devices
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
- block swap notebook has changed
- block swap undo state is unavailable
- attribute view custom color
- attribute view custom color
- block position changed since conversion
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)