{"record":{"id":"c62f046ec65c3a92","repo":"siyuan-note/siyuan","slug":"encrypted-document-history-root-id-does-not-match","errorCode":null,"errorMessage":"encrypted document history root ID does not match its filename","messagePattern":"encrypted document history root ID does not match its filename","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":221,"sourceCode":"\t} else if len(pathParts) >= 2 && IsEncryptedBox(pathParts[1]) {\n\t\terr = fmt.Errorf(\"encrypted notebook document history is plaintext [%s]\", pathParts[1])\n\t\treturn\n\t}\n\tisLargeDoc = 1024*1024*1 <= len(data)\n\n\tluteEngine := NewLute()\n\tif err = treenode.CheckSpecJSON(data); nil != err {\n\t\treturn\n\t}\n\thistoryTree, err := dataparser.ParseJSONWithoutFix(data, luteEngine.ParseOptions)\n\tif err != nil {\n\t\tlogging.LogErrorf(\"parse tree from file [%s] failed: %s\", historyPath, err)\n\t\treturn\n\t}\n\tid = historyTree.Root.ID\n\trootID = historyTree.Root.ID\n\tif ciphertext && rootID+\".sy\" != filepath.Base(historyPath) {\n\t\treturn \"\", \"\", \"\", false, errors.New(\"encrypted document history root ID does not match its filename\")\n\t}\n\n\tif !isLargeDoc {\n\t\trenderTree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument}}\n\t\tkeyword = strings.Join(strings.Split(keyword, \" \"), search.TermSep)\n\t\tkeywords := search.SplitKeyword(keyword)\n\n\t\tvar unlinks []*ast.Node\n\t\tast.Walk(historyTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {\n\t\t\tif !entering {\n\t\t\t\treturn ast.WalkContinue\n\t\t\t}\n\n\t\t\t// 数据历史浏览时忽略内容块折叠状态 https://github.com/siyuan-note/siyuan/issues/5778\n\t\t\tn.RemoveIALAttr(\"heading-fold\")\n\t\t\tn.RemoveIALAttr(\"fold\")\n\n\t\t\tif highlight && 0 < len(keywords) {","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/history.go#L203-L239","documentation":"GetDocHistoryContent reads a historical .sy snapshot and, when the source notebook is encrypted, verifies that the decrypted tree's root block ID matches the history filename (<rootID>.sy). If the decrypted document's root ID differs from the filename, the snapshot is inconsistent (renamed, corrupted, or wrongly decrypted) and the function aborts rather than serve content that cannot be traced back to the named document.","triggerScenarios":"Calling getDocHistoryContent (kernel API history/getDocHistoryContent) for a history file under an encrypted notebook whose decrypted root.ID plus '.sy' does not equal filepath.Base(historyPath); e.g. a history file that was renamed, copied between documents, or whose ciphertext decrypts with the wrong/derived key producing a mismatched tree.","commonSituations":"Hand-editing or moving files inside data/history/; restoring history files from another workspace or notebook; DEK/key rotation leaving stale ciphertext; history snapshots taken before a document ID was regenerated.","solutions":["Verify the history file was not manually renamed: the basename must equal <rootID>.sy of the document inside","Re-generate history for the document (edit and let the auto-history snapshot run) instead of copying old snapshots","Confirm the notebook's DEK is the original one — decrypting with a wrong key yields a tree whose ID won't match","Remove the inconsistent snapshot file if it is unrecoverable and rely on other history entries"],"exampleFix":"// before: renamed history file breaks lookup\nmv data/history/20260910/boxID/oldid.sy data/history/20260910/boxID/newname.sy\n// after: keep filename in sync with root ID\ncp data/history/20260910/boxID/<rootID>.sy backup/ && rm data/history/20260910/boxID/newname.sy","handlingStrategy":"validation","validationCode":"const base = historyPath.split(\"/\").pop()\n// before rolling forward to content rendering, confirm the snapshot belongs to the doc\nconst docRootId = docId // known document ID\nif (!base || !base.startsWith(docRootId) || !base.endsWith(\".sy\")) throw new Error(\"history filename does not match document ID\")","typeGuard":"function isConsistentHistoryPath(historyPath, rootId) {\n  return typeof historyPath === \"string\" &&\n    historyPath.endsWith(`/${rootId}.sy`)\n}","tryCatchPattern":"try {\n  const res = await fetchPost(\"/api/history/getDocHistoryContent\", {historyPath})\n} catch (e) {\n  if (String(e.msg || e).includes(\"does not match its filename\")) {\n    // skip this snapshot; the file was renamed or corrupted\n  }\n}","preventionTips":["Never manually rename or move files under data/history/","Let SiYuan generate history snapshots; avoid copying snapshots between notebooks","Keep notebook DEKs unchanged; rotate keys only with an explicit migration"],"tags":["go","history","encryption","integrity-check"],"backgroundTag":"checksum-mismatch","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}