{"record":{"id":"61937e1d1667283a","repo":"siyuan-note/siyuan","slug":"encrypted-notebook-is-not-unlocked","errorCode":null,"errorMessage":"encrypted notebook is not unlocked","messagePattern":"encrypted notebook is not unlocked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/crypto_lifecycle.go","lineNumber":203,"sourceCode":"\tlifecycle.acceptOperations = false\n\tfor lifecycle.activeOperations > 0 {\n\t\tlifecycle.condition.Wait()\n\t}\n\tlifecycle.lock.Unlock()\n}\n\ntype encryptedBoxOperationScope struct {\n\tlock     sync.Mutex\n\tboxIDs   []string\n\tboxIDSet map[string]struct{}\n\tclosed   bool\n}\n\ntype encryptedBoxOperationScopeKey struct{}\n\nvar (\n\t// ErrEncryptedBoxNotUnlocked 表示加密笔记本当前未解锁。\n\tErrEncryptedBoxNotUnlocked = errors.New(\"encrypted notebook is not unlocked\")\n\t// ErrEncryptedBoxOperationScopeClosed 表示响应级操作作用域已经关闭。\n\tErrEncryptedBoxOperationScopeClosed = errors.New(\"encrypted notebook operation scope is closed\")\n)\n\n// WithEncryptedBoxOperationScope 创建覆盖整个外层响应过程的租约作用域。\nfunc WithEncryptedBoxOperationScope(ctx context.Context) (context.Context, func()) {\n\tscope := &encryptedBoxOperationScope{boxIDSet: map[string]struct{}{}}\n\tscopedContext := context.WithValue(ctx, encryptedBoxOperationScopeKey{}, scope)\n\treturn scopedContext, scope.release\n}\n\n// AcquireEncryptedBoxOperations 按固定顺序取得多个笔记本的响应级租约。\nfunc AcquireEncryptedBoxOperations(ctx context.Context, boxIDs []string) (release func(), err error) {\n\tunique := map[string]struct{}{}\n\tfor _, boxID := range boxIDs {\n\t\tif boxID != \"\" && IsEncryptedBox(boxID) {\n\t\t\tunique[boxID] = struct{}{}\n\t\t}","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/crypto_lifecycle.go#L185-L221","documentation":"`ErrEncryptedBoxNotUnlocked` is the sentinel error returned when an operation tries to acquire a lease on an encrypted notebook whose lifecycle state is not `EncryptedBoxStateUnlocked` or which is not currently accepting operations. It signals that the notebook must be unlocked (typically with the master password) before the requested operation can proceed.","triggerScenarios":"Calling AcquireEncryptedBoxOperation / AcquireEncryptedBoxOperations (e.g. from MCP server.go:284 tool handlers) while the notebook is locked or during a lifecycle transition where acceptOperations is false.","commonSituations":"Running an MCP tool against an encrypted notebook that has not been unlocked since kernel start; the notebook was re-locked by idle timeout or manually; concurrent lock operation rejected leases.","solutions":["Unlock the encrypted notebook first via the unlock API/UI with the master password, then retry the operation.","Check errors.Is(err, model.ErrEncryptedBoxNotUnlocked) and surface a user-facing 'please unlock' message as the MCP layer does.","Avoid scheduling automated operations (sync/export/leases) on notebooks known to be locked; query lock state beforehand.","If the notebook should already be unlocked, verify no re-lock happened (timeout/manual) between unlock and the call."],"exampleFix":"// before\nboxID, err := model.AcquireEncryptedBoxOperation(id)\nif err != nil { return err }\n// after\nboxID, err := model.AcquireEncryptedBoxOperation(id)\nif err != nil {\n    if errors.Is(err, model.ErrEncryptedBoxNotUnlocked) {\n        return toolErrorResult(\"encrypted notebook is locked, please unlock it first\")\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// Check unlock state before acquiring leases\nif !model.IsEncryptedBoxUnlocked(boxID) {\n    return errors.New(\"unlock the notebook before running this tool\")\n}","typeGuard":null,"tryCatchPattern":"if err := model.AcquireEncryptedBoxOperation(boxID); err != nil {\n    if errors.Is(err, model.ErrEncryptedBoxNotUnlocked) {\n        return toolErrorResult(\"encrypted notebook is locked, please unlock it first\")\n    }\n    return err\n}","preventionTips":["Unlock encrypted notebooks at the start of any batch/scripted operation.","Be aware of auto-relock timeouts; re-check state before long-running jobs.","Surface the sentinel error to users as an 'unlock first' prompt instead of a generic failure."],"tags":["go","encryption","locking","lifecycle"],"backgroundTag":"authentication-required","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"}