siyuan-note/siyuan · error
block swap notebook has changed
Error message
block swap notebook has changed
What it means
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.
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
Defensive patterns
Strategy: try-catch
Validate before calling
for (const f of fragments) {
const tree = trees[f.rootID];
if (!tree || tree.box !== f.boxID) throw new Error('fragment tree ' + f.rootID + ' missing or moved');
} Try / catch
try { await replaySwap(tx); } catch (e) {
if (e.message.includes('block swap notebook has changed')) { /* restore docs/notebooks, or discard stale undo */ }
} Prevention
- Do not delete or move documents involved in a pending swap undo
- Re-sync and reopen affected docs before replaying swap history
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- block changed since conversion
- block swap undo state is unavailable
- attribute view is not unused
- block position changed since conversion
- block swap ID already exists
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/57293691ef093c2b.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/transaction_block_swap.go:227
if !IsSameCryptoBoundary(orderedTrees[0].Box, tree.Box) {
return errors.New("cannot replay block swap across encrypted notebook boundaries")
}
}
tx.saveBlockSwapOriginalTrees(orderedTrees)
if err := restoreBlockSwapFragments(from, to, trees); err != nil {
return err
}
tx.finishBlockSwap(from, to, orderedTrees)
operation.RetData = state.rootIDs
return nil
}
func restoreBlockSwapFragments(from, to []blockSwapFragment, trees map[string]*parse.Tree) error {
removing := map[string]bool{}
for _, fragment := range from {
tree := trees[fragment.rootID]
if tree == nil || tree.Box != fragment.boxID {
return errors.New("block swap notebook has changed")
}
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
}
}View on GitHub (pinned to 9f775e8a12)