siyuan-note/siyuan · error

block position changed since conversion

Error message

block position changed since conversion: %s

What it means

During replay of a block-swap transaction, SiYuan re-verifies each source fragment's position by recomputing its previous/next sibling block IDs and comparing them with the IDs recorded at conversion time. A mismatch means the block's siblings around it moved between conversion and replay, so blindly unlinking it would corrupt the document. The transaction is aborted with this error naming the offending block ID.

Solutions

  1. Refresh the affected document and retry the block swap from a freshly captured state
  2. Sync/merge the notebook so the local tree matches the state the swap was prepared on, then re-run
  3. Inspect block ID in the message to see which sibling changed, and revert or redo that conflicting edit first
  4. If the swap is stale and unwanted, discard the pending transaction instead of forcing replay

Example fix

// before: replaying a swap prepared earlier against a stale tree
replayBlockSwap(tx, operation) // -> block position changed since conversion: 20240101120000-abc

// after: re-capture fragments from the current tree, then replay
state := captureBlockSwapState(tx, rootIDs) // rebuild from/to fragments
replayBlockSwap(tx, operation, state)
Defensive patterns

Strategy: retry

Validate before calling

// Go: verify fragment anchors before replay
prev, next := siblingBlockIDs(tree, fragment.node.ID)
if prev != fragment.previousID || next != fragment.nextID {
    // stale fragment: re-capture swap state and retry
}

Try / catch

// Go
if err := restoreBlockSwapFragments(from, to, trees); err != nil {
    if strings.Contains(err.Error(), "block position changed since conversion") {
        // re-capture fragments from the current trees and retry once
    }
    return err
}

Prevention

When it happens

Trigger: Calling the block-swap replay path (replayBlockSwap -> restoreBlockSwapFragments) when, since the swap fragments were captured, another transaction inserted, removed, or reordered sibling blocks adjacent to fragment.node, so previousID/nextID recomputed from the live tree no longer match the recorded fragment values.

Common situations: Two devices synced changes between the conversion and the replay; the user or a plugin edited the same document concurrently; the swap was prepared on an older snapshot of the tree; delayed/stale transaction replay after bulk undo or import operations.

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


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

Appendix: source

Thrown at kernel/model/transaction_block_swap.go:247

		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() {
			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() {

View on GitHub (pinned to 9f775e8a12)