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
- Refresh the affected document and retry the block swap from a freshly captured state
- Sync/merge the notebook so the local tree matches the state the swap was prepared on, then re-run
- Inspect block ID in the message to see which sibling changed, and revert or redo that conflicting edit first
- 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
- Re-capture swap fragments immediately before replay; never cache them across edits
- Run sync and complete pending transactions before preparing a block swap
- Avoid concurrent edits to documents participating in a swap
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
- block swap ID already exists
- block swap insertion position has changed
- agent runtime turn changed
- agent session revision conflict
- attribute view order changed; retry the drag
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)