windmill-labs/windmill · error
Draft "${path}" changed externally since you last read it; i
Error message
Draft "${path}" changed externally since you last read it; it was not removed. Re-read and retry. What it means
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.
Source
Thrown at frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts:612
}
// `remove`/`clear` only debounce the delete; persist it now so a deploy/discard
// that the caller awaits has actually cleared the server draft on return.
await UserDraftDbSyncer.save({
workspace,
itemKind,
path: storagePath,
value: null,
immediate: true
})
// A failed (network/5xx) or conflicted delete is recorded in the syncer state,
// not thrown — surface it so callers don't report the draft as removed while
// the DB-backed source of truth still has it (same guard as the write path).
const state = UserDraftDbSyncer.getState({ workspace, itemKind, path: storagePath })
if (state.state === 'failed') {
throw new Error(state.failureMessage ?? `Failed to delete draft "${path}".`)
}
if (UserDraftDbSyncer.getConflict({ workspace, itemKind, path: storagePath }).conflict) {
throw new Error(
`Draft "${path}" changed externally since you last read it; it was not removed. Re-read and retry.`
)
}
invalidateWorkspaceDrafts(workspace)
}
/** Kind-addressed live-editor storage resolution (friendly → storage path),
* for callers that must probe several draft kinds per chat type. */
export function resolveGlobalDraftStoragePathByKind(
workspace: string,
itemKind: UserDraftItemKind,
path: string
): string {
return resolveDraftStoragePath(workspace, itemKind, path)
}
/** Local in-memory draft cell, kind-addressed: the chat `app` type spans two
* draft kinds (raw_app + classic app), so callers probing both address byView on GitHub (pinned to e474e8803c)
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.
Example fix
// before
await deleteGlobalDraft(ws, 'flow', path)
// after
try {
await deleteGlobalDraft(ws, 'flow', path)
} catch (e) {
if (String(e).includes('changed externally')) {
await readGlobalDraft(ws, 'flow', path) // refresh + confirm, then retry
await deleteGlobalDraft(ws, 'flow', path)
} else throw e
} Defensive patterns
Strategy: retry
Validate before calling
const { conflict } = UserDraftDbSyncer.getConflict({ workspace, itemKind, path })
if (conflict) {
await readGlobalDraft(workspace, type, path) // refresh local copy before deleting
} Try / catch
try {
await deleteGlobalDraft(workspace, itemKind, path)
} catch (e) {
if (e instanceof Error && e.message.includes('changed externally')) {
const ok = await confirmExternalChangeAndReRead()
if (ok) await deleteGlobalDraft(workspace, itemKind, path)
} else throw e
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- This draft has a newer conflicting version on the server. Re
- Failed to cache esbuild-wasm@${version} at ${destDir}
- Cannot deploy ${label}: the draft has a conflicting newer ve
- state.failureMessage ?? `Failed to delete draft "${path}".`
- res.error ?? 'discard failed'
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/9758c439111cdce2.
Report an issue: GitHub.