{"record":{"id":"5b266bf944a94077","repo":"siyuan-note/siyuan","slug":"block-position-changed-since-conversion-s","errorCode":null,"errorMessage":"block position changed since conversion: %s","messagePattern":"block position changed since conversion: (.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/transaction_block_swap.go","lineNumber":247,"sourceCode":"\t\tnode := treenode.GetNodeInTree(tree, fragment.node.ID)\n\t\tif node == nil || node.Parent != tree.Root || blockSwapFingerprint(node) != blockSwapFingerprint(fragment.node) {\n\t\t\treturn fmt.Errorf(\"block changed since conversion: %s\", fragment.node.ID)\n\t\t}\n\t\tpreviousID, nextID := \"\", \"\"\n\t\tfor previous := node.Previous; previous != nil; previous = previous.Previous {\n\t\t\tif previous.IsBlock() && previous.ID != \"\" {\n\t\t\t\tpreviousID = previous.ID\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor next := node.Next; next != nil; next = next.Next {\n\t\t\tif next.IsBlock() && next.ID != \"\" {\n\t\t\t\tnextID = next.ID\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif previousID != fragment.previousID || nextID != fragment.nextID {\n\t\t\treturn fmt.Errorf(\"block position changed since conversion: %s\", fragment.node.ID)\n\t\t}\n\t\tfor _, id := range node.BlockIDs() {\n\t\t\tremoving[id] = true\n\t\t}\n\t}\n\tavailable := map[string]bool{}\n\tfor _, fragment := range to {\n\t\ttree := trees[fragment.rootID]\n\t\tif tree == nil || tree.Box != fragment.boxID {\n\t\t\treturn errors.New(\"block swap notebook has changed\")\n\t\t}\n\t\tif fragment.previousID != \"\" && !available[fragment.previousID] {\n\t\t\tprevious := treenode.GetNodeInTree(tree, fragment.previousID)\n\t\t\tif previous == nil || previous.Parent != tree.Root || removing[previous.ID] {\n\t\t\t\treturn errors.New(\"block swap insertion position has changed\")\n\t\t\t}\n\t\t}\n\t\tfor _, id := range fragment.node.BlockIDs() {","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/transaction_block_swap.go#L229-L265","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: replaying a swap prepared earlier against a stale tree\nreplayBlockSwap(tx, operation) // -> block position changed since conversion: 20240101120000-abc\n\n// after: re-capture fragments from the current tree, then replay\nstate := captureBlockSwapState(tx, rootIDs) // rebuild from/to fragments\nreplayBlockSwap(tx, operation, state)","handlingStrategy":"retry","validationCode":"// Go: verify fragment anchors before replay\nprev, next := siblingBlockIDs(tree, fragment.node.ID)\nif prev != fragment.previousID || next != fragment.nextID {\n    // stale fragment: re-capture swap state and retry\n}","typeGuard":null,"tryCatchPattern":"// Go\nif err := restoreBlockSwapFragments(from, to, trees); err != nil {\n    if strings.Contains(err.Error(), \"block position changed since conversion\") {\n        // re-capture fragments from the current trees and retry once\n    }\n    return err\n}","preventionTips":["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"],"tags":["go","transaction","concurrency","block-swap"],"backgroundTag":"invalid-state-transition","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}