{"record":{"id":"57293691ef093c2b","repo":"siyuan-note/siyuan","slug":"block-swap-notebook-has-changed","errorCode":null,"errorMessage":"block swap notebook has changed","messagePattern":"block swap notebook has changed","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/transaction_block_swap.go","lineNumber":227,"sourceCode":"\t\tif !IsSameCryptoBoundary(orderedTrees[0].Box, tree.Box) {\n\t\t\treturn errors.New(\"cannot replay block swap across encrypted notebook boundaries\")\n\t\t}\n\t}\n\ttx.saveBlockSwapOriginalTrees(orderedTrees)\n\tif err := restoreBlockSwapFragments(from, to, trees); err != nil {\n\t\treturn err\n\t}\n\ttx.finishBlockSwap(from, to, orderedTrees)\n\toperation.RetData = state.rootIDs\n\treturn nil\n}\n\nfunc restoreBlockSwapFragments(from, to []blockSwapFragment, trees map[string]*parse.Tree) error {\n\tremoving := map[string]bool{}\n\tfor _, fragment := range from {\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\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}","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/transaction_block_swap.go#L209-L245","documentation":"restoreBlockSwapFragments re-checks, for each stored fragment, that its tree still exists in the provided map and still belongs to the same notebook (box ID) recorded at swap time. This error means a fragment's tree is missing from the snapshot or was moved to another notebook, so the swap cannot be safely replayed or finalized without corrupting content.","triggerScenarios":"Replaying a block swap after a fragment tree was deleted, or after the tree's root was moved to a different notebook so tree.Box != fragment.boxID; passing a trees map that omits one of the fragment roots.","commonSituations":"Undoing a swap after deleting a involved document; a tree moved between notebooks by a concurrent operation or another device via sync; partial workspace restore.","solutions":["Restore the deleted/moved document(s) to their original notebooks, then retry","Refresh/reopen the documents so the in-memory trees match disk state before replaying","Discard the stale undo entry; the swap state is no longer valid"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"for (const f of fragments) {\n  const tree = trees[f.rootID];\n  if (!tree || tree.box !== f.boxID) throw new Error('fragment tree ' + f.rootID + ' missing or moved');\n}","typeGuard":null,"tryCatchPattern":"try { await replaySwap(tx); } catch (e) {\n  if (e.message.includes('block swap notebook has changed')) { /* restore docs/notebooks, or discard stale undo */ }\n}","preventionTips":["Do not delete or move documents involved in a pending swap undo","Re-sync and reopen affected docs before replaying swap history"],"tags":["replay","block-swap","state"],"backgroundTag":"stale-state-conflict","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"}