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

  1. Search the notebook for the conflicting block ID in the message and delete or re-id the duplicate block
  2. Ensure the block swap is not replayed twice; clear duplicate/stale pending transactions
  3. Run a reindex (Reindex) to reconcile the block-tree index with actual .sy files, then retry
  4. 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

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


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)