siyuan-note/siyuan · error
block swap insertion position has changed
Error message
block swap insertion position has changed
What it means
For each 'to' fragment, the replay verifies the recorded insertion anchor (previousID) still exists as a direct child of the document root and is not scheduled for removal. If the anchor block is gone, was reparented deeper, or is itself being swapped away, the intended insertion position no longer exists, so the fragment cannot be restored safely and the transaction is aborted.
Solutions
- Check the anchor block (fragment.previousID) still exists as a top-level child and undo changes that removed or nested it
- Sync the notebook to converge both sides, then re-capture and retry the swap
- Re-prepare the block swap against the current document structure
- Discard the stale transaction if the intended position no longer makes sense
Defensive patterns
Strategy: validation
Validate before calling
// Go: ensure the insertion anchor is still a root-level child and not being removed
previous := treenode.GetNodeInTree(tree, fragment.previousID)
if previous == nil || previous.Parent != tree.Root || removing[previous.ID] {
return errors.New("block swap insertion position has changed")
} Prevention
- Do not delete or nest the anchor block while a swap is pending
- Re-capture swap state after any structural edit to the document
- Converge sync before replaying multi-device transactions
When it happens
Trigger: replayBlockSwap -> restoreBlockSwapFragments: fragment.previousID is set, not yet marked available, and treenode.GetNodeInTree returns nil, the node's parent is not tree.Root, or the node is in the removing set — i.e. the anchor sibling was deleted, moved, or is being removed by the same swap.
Common situations: Concurrent deletion of the block next to the insertion point; an import/undo that restructured the document; ordering conflicts after multi-device sync; replaying an old swap whose anchor was since moved into a nested container.
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
- block position changed since conversion
- block swap ID already exists
- template document tree plan contains an invalid document…
- template document tree plan contains an invalid parent undo…
- template document tree plan contains an invalid parent…
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/7e60dff773a591da.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/transaction_block_swap.go:262
}
}
if previousID != fragment.previousID || nextID != fragment.nextID {
return fmt.Errorf("block position changed since conversion: %s", fragment.node.ID)
}
for _, id := range node.BlockIDs() {
removing[id] = true
}
}
available := map[string]bool{}
for _, fragment := range to {
tree := trees[fragment.rootID]
if tree == nil || tree.Box != fragment.boxID {
return errors.New("block swap notebook has changed")
}
if fragment.previousID != "" && !available[fragment.previousID] {
previous := treenode.GetNodeInTree(tree, fragment.previousID)
if previous == nil || previous.Parent != tree.Root || removing[previous.ID] {
return errors.New("block swap insertion position has changed")
}
}
for _, id := range fragment.node.BlockIDs() {
if existing := treenode.GetBlockTreeInBox(id, fragment.boxID); existing != nil && !removing[id] {
return fmt.Errorf("block swap ID already exists: %s", id)
}
for _, currentTree := range trees {
if treenode.GetNodeInTree(currentTree, id) != nil && !removing[id] {
return fmt.Errorf("block swap ID already exists: %s", id)
}
}
}
available[fragment.node.ID] = true
}
for _, fragment := range from {
treenode.GetNodeInTree(trees[fragment.rootID], fragment.node.ID).Unlink()
}
for _, fragment := range to {View on GitHub (pinned to 9f775e8a12)