{"record":{"id":"1ec43a09f141161e","repo":"siyuan-note/siyuan","slug":"template-changed-reload-it-before-saving-moving","errorCode":null,"errorMessage":"template changed; reload it before saving, moving or deleting","messagePattern":"template changed; reload it before saving, moving or deleting","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_manage.go","lineNumber":274,"sourceCode":"\t\tif !strings.EqualFold(path.Ext(request.Path), \".md\") {\n\t\t\treturn nil, errors.New(\"template source must use the .md extension\")\n\t\t}\n\t}\n\tif request.Action == \"read\" {\n\t\tif info.IsDir() {\n\t\t\trevision, readErr := templateFileRevision(root, request.Path)\n\t\t\treturn map[string]string{\"content\": \"\", \"revision\": revision}, readErr\n\t\t}\n\t\tcontent, readErr := readTemplateSource(root, request.Path)\n\t\treturn map[string]string{\"content\": content, \"revision\": fmt.Sprintf(\"%x\", sha256.Sum256([]byte(content))), \"path\": filepath.Join(util.DataDir, \"templates\", filepath.FromSlash(request.Path))}, readErr\n\t}\n\tif info != nil {\n\t\trevision, revisionErr := templateFileRevision(root, request.Path)\n\t\tif revisionErr != nil {\n\t\t\treturn nil, revisionErr\n\t\t}\n\t\tif request.Revision == \"\" || request.Revision != revision {\n\t\t\treturn nil, errors.New(\"template changed; reload it before saving, moving or deleting\")\n\t\t}\n\t}\n\tswitch request.Action {\n\tcase \"write\":\n\t\tif info == nil {\n\t\t\tif err = validateNewTemplateName(request.Path); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif len(request.Content) > maxTemplateSourceSize {\n\t\t\treturn nil, errors.New(\"template source is too large\")\n\t\t}\n\t\tif info != nil && info.IsDir() {\n\t\t\treturn nil, errors.New(\"cannot write a template directory\")\n\t\t}\n\t\terr = writeTemplateSource(root, request.Path, request.Content, info == nil)\n\t\treturn map[string]string{\"revision\": fmt.Sprintf(\"%x\", sha256.Sum256([]byte(request.Content)))}, err\n\tcase \"move\":","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/template_manage.go#L256-L292","documentation":"ManageTemplateFiles implements optimistic concurrency: for any existing path (info != nil) it recomputes a SHA-256 revision of the file/directory and requires the caller to pass that exact value in request.Revision before allowing write, move, or remove. If the revision is missing or stale — meaning someone else modified the template since the caller last read it — the operation is rejected so conflicting edits do not silently overwrite each other.","triggerScenarios":"Any write/move/remove on an existing template where request.Revision is empty, or differs from the current hash: another window/tab saved the template in between, a sync service touched the file (mtime/size change), the caller cached a revision from before an external edit, or a directory's contents changed (any entry added/removed/renamed changes the directory revision).","commonSituations":"Two editor tabs editing the same template; a WebDAV/cloud sync updates the file between read and save; a long-lived client holds a revision across an external re-save; the user edited the file on disk while the app had it open.","solutions":["Re-read the template (action=\"read\") to get the fresh revision and your desired content, then retry the write/move/remove with the new Revision","If your change should win, merge your edits into the freshly read content before re-saving — do not blindly retry with the old content","Check what else is touching data/templates (sync clients, other SiYuan windows) and avoid concurrent edits"],"exampleFix":"// before (stale revision)\n{ \"action\": \"write\", \"path\": \"t.md\", \"revision\": \"abc123...\", \"content\": \"new text\" }\n// after: first\n{ \"action\": \"read\", \"path\": \"t.md\" }  -> revision \"def456...\"\n// then\n{ \"action\": \"write\", \"path\": \"t.md\", \"revision\": \"def456...\", \"content\": \"merged text\" }","handlingStrategy":"retry","validationCode":"// always carry the revision from your last read into any mutating call\nconst { revision } = await manageTemplateFiles({ action: 'read', path: p });\nawait manageTemplateFiles({ action: 'write', path: p, revision, content });","typeGuard":null,"tryCatchPattern":"try {\n  await saveTemplate(p, content, cachedRev);\n} catch (e) {\n  if (String(e.message).includes('reload it before')) {\n    const fresh = await manageTemplateFiles({ action: 'read', path: p });\n    const merged = mergeEdits(fresh.content, content); // never blindly overwrite\n    await saveTemplate(p, merged, fresh.revision);\n  }\n}","preventionTips":["Never cache a revision across user think-time; re-read right before saving","Avoid editing the same template in multiple windows/tabs","Pause or scope sync clients while performing bulk template operations","Treat this error as a merge-conflict signal, not a transient failure — re-read and merge"],"tags":["concurrency","optimistic-locking","template","conflict"],"backgroundTag":"stale-revision-conflict","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}