siyuan-note/siyuan · warning

encrypted notebook is not unlocked

Error message

encrypted notebook is not unlocked

What it means

`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.

Source

Thrown at kernel/model/crypto_lifecycle.go:203

	lifecycle.acceptOperations = false
	for lifecycle.activeOperations > 0 {
		lifecycle.condition.Wait()
	}
	lifecycle.lock.Unlock()
}

type encryptedBoxOperationScope struct {
	lock     sync.Mutex
	boxIDs   []string
	boxIDSet map[string]struct{}
	closed   bool
}

type encryptedBoxOperationScopeKey struct{}

var (
	// ErrEncryptedBoxNotUnlocked 表示加密笔记本当前未解锁。
	ErrEncryptedBoxNotUnlocked = errors.New("encrypted notebook is not unlocked")
	// ErrEncryptedBoxOperationScopeClosed 表示响应级操作作用域已经关闭。
	ErrEncryptedBoxOperationScopeClosed = errors.New("encrypted notebook operation scope is closed")
)

// WithEncryptedBoxOperationScope 创建覆盖整个外层响应过程的租约作用域。
func WithEncryptedBoxOperationScope(ctx context.Context) (context.Context, func()) {
	scope := &encryptedBoxOperationScope{boxIDSet: map[string]struct{}{}}
	scopedContext := context.WithValue(ctx, encryptedBoxOperationScopeKey{}, scope)
	return scopedContext, scope.release
}

// AcquireEncryptedBoxOperations 按固定顺序取得多个笔记本的响应级租约。
func AcquireEncryptedBoxOperations(ctx context.Context, boxIDs []string) (release func(), err error) {
	unique := map[string]struct{}{}
	for _, boxID := range boxIDs {
		if boxID != "" && IsEncryptedBox(boxID) {
			unique[boxID] = struct{}{}
		}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Unlock the encrypted notebook first via the unlock API/UI with the master password, then retry the operation.
  2. Check errors.Is(err, model.ErrEncryptedBoxNotUnlocked) and surface a user-facing 'please unlock' message as the MCP layer does.
  3. Avoid scheduling automated operations (sync/export/leases) on notebooks known to be locked; query lock state beforehand.
  4. If the notebook should already be unlocked, verify no re-lock happened (timeout/manual) between unlock and the call.

Example fix

// before
boxID, err := model.AcquireEncryptedBoxOperation(id)
if err != nil { return err }
// after
boxID, err := model.AcquireEncryptedBoxOperation(id)
if err != nil {
    if errors.Is(err, model.ErrEncryptedBoxNotUnlocked) {
        return toolErrorResult("encrypted notebook is locked, please unlock it first")
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Check unlock state before acquiring leases
if !model.IsEncryptedBoxUnlocked(boxID) {
    return errors.New("unlock the notebook before running this tool")
}

Try / catch

if err := model.AcquireEncryptedBoxOperation(boxID); err != nil {
    if errors.Is(err, model.ErrEncryptedBoxNotUnlocked) {
        return toolErrorResult("encrypted notebook is locked, please unlock it first")
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/61937e1d1667283a. Report an issue: GitHub.