{"record":{"id":"9758c439111cdce2","repo":"windmill-labs/windmill","slug":"draft-path-changed-externally-since-you-last","errorCode":null,"errorMessage":"Draft \"${path}\" changed externally since you last read it; it was not removed. Re-read and retry.","messagePattern":"Draft \"(.+?)\" changed externally since you last read it; it was not removed\\. Re-read and retry\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts","lineNumber":612,"sourceCode":"\t}\n\t// `remove`/`clear` only debounce the delete; persist it now so a deploy/discard\n\t// that the caller awaits has actually cleared the server draft on return.\n\tawait UserDraftDbSyncer.save({\n\t\tworkspace,\n\t\titemKind,\n\t\tpath: storagePath,\n\t\tvalue: null,\n\t\timmediate: true\n\t})\n\t// A failed (network/5xx) or conflicted delete is recorded in the syncer state,\n\t// not thrown — surface it so callers don't report the draft as removed while\n\t// the DB-backed source of truth still has it (same guard as the write path).\n\tconst state = UserDraftDbSyncer.getState({ workspace, itemKind, path: storagePath })\n\tif (state.state === 'failed') {\n\t\tthrow new Error(state.failureMessage ?? `Failed to delete draft \"${path}\".`)\n\t}\n\tif (UserDraftDbSyncer.getConflict({ workspace, itemKind, path: storagePath }).conflict) {\n\t\tthrow new Error(\n\t\t\t`Draft \"${path}\" changed externally since you last read it; it was not removed. Re-read and retry.`\n\t\t)\n\t}\n\tinvalidateWorkspaceDrafts(workspace)\n}\n\n/** Kind-addressed live-editor storage resolution (friendly → storage path),\n * for callers that must probe several draft kinds per chat type. */\nexport function resolveGlobalDraftStoragePathByKind(\n\tworkspace: string,\n\titemKind: UserDraftItemKind,\n\tpath: string\n): string {\n\treturn resolveDraftStoragePath(workspace, itemKind, path)\n}\n\n/** Local in-memory draft cell, kind-addressed: the chat `app` type spans two\n * draft kinds (raw_app + classic app), so callers probing both address by","sourceCodeStart":594,"sourceCodeEnd":630,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts#L594-L630","documentation":"deleteGlobalDraft() guards against concurrent modification: after the delete request, it consults UserDraftDbSyncer.getConflict(); if the syncer recorded that the draft changed externally since it was last read, the delete is not applied and this error is thrown with explicit retry guidance. This is an optimistic-concurrency conflict, not a transport failure — the draft still exists server-side.","triggerScenarios":"Another client/editor (live editor, another browser tab, a deploy) modified the draft between your read and your delete, so the syncer's stored revision no longer matches the server's and getConflict().conflict is true.","commonSituations":"Two team members editing the same script/flow draft; the chat deleting a draft while the user has unsaved edits in the live editor; a stale chat session operating on a draft someone rebased moments ago.","solutions":["Re-read the draft (getGlobalDraft) to pick up the external changes, then retry deleteGlobalDraft.","If the external change should be kept, discard the deletion instead of retrying.","Use rebase*Draft helpers (rebaseScriptDraft/rebaseFlowDraft/rebaseAppDraft) to merge, then delete.","Coordinate with the other editor/session to avoid simultaneous writes."],"exampleFix":"// before\nawait deleteGlobalDraft(ws, 'flow', path)\n// after\ntry {\n  await deleteGlobalDraft(ws, 'flow', path)\n} catch (e) {\n  if (String(e).includes('changed externally')) {\n    await readGlobalDraft(ws, 'flow', path) // refresh + confirm, then retry\n    await deleteGlobalDraft(ws, 'flow', path)\n  } else throw e\n}","handlingStrategy":"retry","validationCode":"const { conflict } = UserDraftDbSyncer.getConflict({ workspace, itemKind, path })\nif (conflict) {\n  await readGlobalDraft(workspace, type, path) // refresh local copy before deleting\n}","typeGuard":null,"tryCatchPattern":"try {\n  await deleteGlobalDraft(workspace, itemKind, path)\n} catch (e) {\n  if (e instanceof Error && e.message.includes('changed externally')) {\n    const ok = await confirmExternalChangeAndReRead()\n    if (ok) await deleteGlobalDraft(workspace, itemKind, path)\n  } else throw e\n}","preventionTips":["Re-read drafts shortly before mutating them in long-lived chat sessions","Warn users when another editor/session has the same draft open","Prefer rebase*Draft flows over blind delete when external edits are likely"],"tags":["concurrency","optimistic-locking","drafts"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}