siyuan-note/siyuan · error
block swap ID already exists
Error message
block swap ID already exists: %s
What it means
Before inserting 'to' fragments, the replay checks that no block ID carried by the fragment already exists in the notebook's block-tree index (treenode.GetBlockTreeInBox), unless that ID is being removed by the same swap. A surviving duplicate means the fragment's IDs were taken by another block (e.g. via duplicate-doc or sync divergence), and inserting would create duplicate block IDs — illegal in SiYuan — so the replay aborts naming the conflicting ID.
Solutions
- Search the notebook for the conflicting block ID in the message and delete or re-id the duplicate block
- Ensure the block swap is not replayed twice; clear duplicate/stale pending transactions
- Run a reindex (Reindex) to reconcile the block-tree index with actual .sy files, then retry
- Sync and resolve document duplication conflicts before replaying the swap
Defensive patterns
Strategy: validation
Validate before calling
// Go: detect live IDs before replay
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)
}
} Prevention
- Never replay a block-swap transaction twice; track applied transaction IDs
- Resolve document-duplication conflicts from sync before replaying swaps
- Reindex the notebook if the block index may be stale relative to .sy files
When it happens
Trigger: replayBlockSwap -> restoreBlockSwapFragments: one of fragment.node.BlockIDs() resolves via treenode.GetBlockTreeInBox(id, fragment.boxID) to an existing block-tree row that is not in the removing set — i.e. the ID is live in the same notebook outside the blocks this swap removes.
Common situations: Duplicating a document creates blocks with copied IDs that collide with pending swap fragments; sync divergence duplicated content across devices; a partially applied earlier transaction left the fragment IDs in the index; the swap was replayed twice.
Related errors
- block position changed since conversion
- block swap insertion position has changed
- 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/d616c02062327cae.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/transaction_block_swap.go:267
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 {
tree := trees[fragment.rootID]
node := cloneBlockSwapNode(fragment.node)
if fragment.previousID == "" {
tree.Root.PrependChild(node)
} else {View on GitHub (pinned to 9f775e8a12)