{"record":{"id":"bd36f969559fb489","repo":"laurent22/joplin","slug":"cannot-change-the-note-lock-of-a-conflict-note","errorCode":null,"errorMessage":"Cannot change the note lock of a conflict note","messagePattern":"Cannot change the note lock of a conflict note","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/lib/services/noteLock/setNoteLockState.ts","lineNumber":18,"sourceCode":"import { ModelType } from '../../BaseModel';\nimport 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);","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/laurent22/joplin/blob/981a03c5c9e88130bccff4db47c411d35ec7ae2c/packages/lib/services/noteLock/setNoteLockState.ts#L1-L36","documentation":"A generic validation guard in checkCanChangeLockState that rejects any attempt to enable or disable a lock on a note whose type is NOTE_TYPE_CONFLICT. Conflict notes are temporary merge/sync artifacts that Joplin manages internally, so a user- or plugin-driven lock change on them is never valid; the input at fault is a conflict-type NoteEntity passed to enableNoteLock or disableNoteLock (e.g. via commands invoked from keyboard shortcuts or plugins, since the UI normally hides these actions for conflict notes).","triggerScenarios":"Invoking enableNoteLock/disableNoteLock on a note with `is_conflict === true`, which appears in the conflict folder and is produced by sync conflict resolution.","commonSituations":"Plugins processing all notes in the conflict folder; automation that locks recently modified notes after a sync generated conflicts.","solutions":["Resolve the conflict (merge/choose a version) so the note is no longer a conflict copy, then lock the resolved note.","Skip notes with `is_conflict` set when batch-locking.","Catch the error and ignore conflict notes.","Lock the original note rather than its conflict copy."],"exampleFix":"// before\nawait joplin.commands.execute('enableNoteLock', conflictNoteId);\n// after\nconst note = await Note.load(conflictNoteId);\nif (note && !note.is_conflict) await joplin.commands.execute('enableNoteLock', conflictNoteId);","handlingStrategy":"validation","validationCode":"const note = await Note.load(noteId);\nif (note && !note.is_conflict) await joplin.commands.execute('enableNoteLock', noteId);","typeGuard":"const isResolvedNote = (n: NoteEntity): boolean => !n.is_conflict;","tryCatchPattern":"try { await enableNoteLock(id); } catch (e) { if (e.message.includes('conflict note')) notify('Resolve conflict first'); else throw e; }","preventionTips":["Resolve sync conflicts before locking","Skip notes in the conflict folder in automation","Handle conflict notes as sync-system-managed"],"tags":["note-lock","conflict","sync","validation"],"backgroundTag":"invalid-state-transition","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"}