windmill-labs/windmill · error · ArtifactPersistenceError
ArtifactPersistenceError
Error message
ArtifactPersistenceError
What it means
ArtifactPersistenceError is thrown by artifactsState.create when a plan-role artifact was created in memory but mutateArtifact did not report outcome 'saved' — i.e. the IndexedDB write failed or was unavailable. For ordinary artifacts an unpersisted result degrades silently, but a plan cannot: returning one the database refused would let the user approve a plan that vanishes on reload, so it is raised instead.
Source
Thrown at frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts:183
updatedAt: now,
version: 1
})
// A plan's id is the session's, so a second one cannot be minted; the slot check happens
// on the row this write is about to replace, inside the transaction that replaces it.
const id = input.role === 'plan' ? planArtifactId(sessionId) : randomUUID()
const { outcome, artifact } = await mutateArtifact(id, (existing) => {
// Before the slot check: a posture that may not mint a plan at all is the more useful
// thing to say, and it holds whether or not the session already has one.
if (input.role === 'plan' && opts?.canWritePlan?.() === false) {
throw new PlanWriteRefusedError()
}
if (existing) throw new PlanSlotTakenError(existing)
const created = draft(id)
return { artifact: created, snapshots: [snapshotOf(created, 1)] }
})
// An ordinary artifact degrades unpersisted; a plan cannot. Returning one the database
// refused would let the user approve a plan that disappears on reload.
if (input.role === 'plan' && outcome !== 'saved') throw new ArtifactPersistenceError()
const written = artifact ?? draft(id)
this.#reflect(written)
return written
}
/**
* Merge changes into an existing artifact. Returns undefined if `id` is unknown, or if
* `opts.sessionId` is given and the artifact belongs to a different session.
*/
async update(
id: string,
input: UpdateArtifactInput,
opts?: { sessionId?: string; canWritePlan?: () => boolean }
): Promise<PersistedArtifact | undefined> {
let refused = false
const { outcome, artifact } = await mutateArtifact(id, (stored) => {
// Read inside the mutator: hoisted out, it would weigh a stale copy against a fresh one.
const held = this.artifacts.find((a) => a.id === id)View on GitHub (pinned to e474e8803c)
Solutions
- Check that IndexedDB is writable (repeat the write or inspect getDB()/console errors) and retry the plan creation after storage recovers
- Free storage or reduce plan content if quota exceeded triggered the failed save
- Handle ArtifactPersistenceError in the tool wrapper so the model is told the plan was not saved instead of presenting an unsavable plan to the user for approval
Example fix
// before
const plan = await artifacts.create({ role: 'plan', sessionId, content })
// after
let plan
try { plan = await artifacts.create({ role: 'plan', sessionId, content }) }
catch (e) {
if (e instanceof ArtifactPersistenceError) { notifyPlanNotSaved(); return }
throw e
} Defensive patterns
Strategy: try-catch
Validate before calling
const db = await getDB()
if (!db) throw new Error('Cannot create plan: artifact store unavailable') Type guard
function isArtifactPersistenceError(e: unknown): e is ArtifactPersistenceError {
return e instanceof ArtifactPersistenceError
} Try / catch
try {
await artifacts.create({ role: 'plan', sessionId, content })
} catch (e) {
if (e instanceof ArtifactPersistenceError) {
toolCallbacks.report('Plan could not be saved; do not ask the user to approve it')
return
}
throw e
} Prevention
- Never present a plan for approval unless persistence reported outcome 'saved'
- Watch IndexedDB quota/availability and warn before long planning sessions in private mode
- Retry the create once on transient IndexedDB failures before surfacing the error
When it happens
Trigger: Creating a plan artifact in a browser context where IndexedDB is unavailable or the transaction fails (quota exceeded, DB closed, private mode), so mutateArtifact returns outcome other than 'saved'.
Common situations: Private-browsing storage restrictions; IndexedDB quota exceeded by large plan documents; browser killing the DB mid-session; extensions blocking storage writes.
Related errors
- The session plan document may not be written here
- PlanSlotTakenError
- AI response was empty
- AI response contained empty code block
- AI response did not contain valid code. Please try rephrasin
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/84c5874e7565bb5b.
Report an issue: GitHub.