{"record":{"id":"f1663c0eb8af9d04","repo":"laurent22/joplin","slug":"cannot-change-the-note-lock-while-the-session-is-locked","errorCode":null,"errorMessage":"Cannot change the note lock while the session is locked","messagePattern":"Cannot change the note lock while the session is locked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/lib/services/noteLock/setNoteLockState.ts","lineNumber":20,"sourceCode":"import BaseItem from '../../models/BaseItem';\nimport ItemChange from '../../models/ItemChange';\nimport Note from '../../models/Note';\nimport Setting from '../../models/Setting';\nimport { itemIsReadOnlySync, ItemSlice } from '../../models/utils/readOnly';\nimport { NoteEntity } from '../database/types';\nimport eventManager, { EventName } from '../../eventManager';\nimport isNoteLockEnabled from './isNoteLockEnabled';\nimport NoteLockSession from './NoteLockSession';\n\n// The UI hides the enable/disable actions for these cases, but the commands can also be\n// invoked directly (keyboard, plugins), so the transitions fail closed here too.\nconst checkCanChangeLockState = (note: NoteEntity, noteId: string) => {\n\tif (!isNoteLockEnabled()) throw new Error('Note lock is not enabled');\n\tif (!note) throw new Error(`No such note: ${noteId}`);\n\tif (note.deleted_time) throw new Error('Cannot change the note lock of a deleted note');\n\tif (note.is_conflict) throw new Error('Cannot change the note lock of a conflict note');\n\tif (itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note as ItemSlice, Setting.value('sync.userId'), BaseItem.syncShareCache)) throw new Error('Cannot change the note lock of a read-only note');\n\tif (!NoteLockSession.instance().isUnlocked()) throw new Error('Cannot change the note lock while the session is locked');\n};\n\nconst validationFields = ['id', 'is_locked', 'deleted_time', 'is_conflict', 'share_id'];\n\n// These only validate and emit: the note screen listens for the event and persists the\n// change with a scheduled gated save.\nexport const enableNoteLock = async (noteId: string) => {\n\tconst note = await Note.load(noteId, { fields: validationFields });\n\tcheckCanChangeLockState(note, noteId);\n\tif (note.is_locked) throw new Error(`Note is already locked: ${noteId}`);\n\teventManager.emit(EventName.NoteLockNoteStateChange, { noteId, isLocked: true });\n};\n\nexport const disableNoteLock = async (noteId: string) => {\n\tconst note = await Note.load(noteId, { fields: validationFields });\n\tcheckCanChangeLockState(note, noteId);\n\tif (!note.is_locked) throw new Error(`Note is not locked: ${noteId}`);\n\teventManager.emit(EventName.NoteLockNoteStateChange, { noteId, isLocked: false });","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/laurent22/joplin/blob/981a03c5c9e88130bccff4db47c411d35ec7ae2c/packages/lib/services/noteLock/setNoteLockState.ts#L2-L38","documentation":"A generic guard in checkCanChangeLockState that blocks any lock-state change while a note lock session is active and locked (NoteLockSession.locked is true). Lock sessions exist to make a temporary lock atomic; mutating locks mid-session would break that invariant, so all enableNoteLock/disableNoteLock calls are rejected until the session is unlocked. It fires when a command is invoked during an active session, e.g. from the keyboard or a plugin, because the UI does not gate direct command invocation.","triggerScenarios":"Calling enableNoteLock/disableNoteLock after the NoteLockSession was locked (e.g. timeout or explicit lock) without re-authenticating/unlocking it first.","commonSituations":"Long-running automation whose note-lock session expired mid-run; users who locked the session and then tried to modify another note's lock via commands/plugins.","solutions":["Unlock the note-lock session (authenticate via the lock UI/API) before changing note lock states.","Re-run the workflow so a fresh unlocked session is established.","Check NoteLockSession.instance().isUnlocked() before each transition.","Shorten workflows to fit within the session timeout."],"exampleFix":"// before\nawait joplin.commands.execute('disableNoteLock', noteId);\n// after\nif (NoteLockSession.instance().isUnlocked()) await joplin.commands.execute('disableNoteLock', noteId);","handlingStrategy":"validation","validationCode":"if (NoteLockSession.instance().isUnlocked()) await joplin.commands.execute('enableNoteLock', noteId);","typeGuard":"const sessionUnlocked = (): boolean => NoteLockSession.instance().isUnlocked();","tryCatchPattern":"try { await disableNoteLock(id); } catch (e) { if (e.message.includes('session is locked')) await unlockSession(); else throw e; }","preventionTips":["Authenticate/unlock the session before lock-state workflows","Re-check session state after long-running operations","Handle session timeouts explicitly in automation"],"tags":["note-lock","session","auth"],"backgroundTag":"authentication-required","analyzedSha":"981a03c5c9e88130bccff4db47c411d35ec7ae2c","analyzedAt":"2026-09-17T14:49:40.960Z","contentChangedAt":"2026-09-17T14:49:40.960Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}