{"record":{"id":"84c5874e7565bb5b","repo":"windmill-labs/windmill","slug":"artifactpersistenceerror","errorCode":null,"errorMessage":"ArtifactPersistenceError","messagePattern":"ArtifactPersistenceError","errorType":"exception","errorClass":"ArtifactPersistenceError","httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts","lineNumber":183,"sourceCode":"\t\t\tupdatedAt: now,\n\t\t\tversion: 1\n\t\t})\n\t\t// A plan's id is the session's, so a second one cannot be minted; the slot check happens\n\t\t// on the row this write is about to replace, inside the transaction that replaces it.\n\t\tconst id = input.role === 'plan' ? planArtifactId(sessionId) : randomUUID()\n\t\tconst { outcome, artifact } = await mutateArtifact(id, (existing) => {\n\t\t\t// Before the slot check: a posture that may not mint a plan at all is the more useful\n\t\t\t// thing to say, and it holds whether or not the session already has one.\n\t\t\tif (input.role === 'plan' && opts?.canWritePlan?.() === false) {\n\t\t\t\tthrow new PlanWriteRefusedError()\n\t\t\t}\n\t\t\tif (existing) throw new PlanSlotTakenError(existing)\n\t\t\tconst created = draft(id)\n\t\t\treturn { artifact: created, snapshots: [snapshotOf(created, 1)] }\n\t\t})\n\t\t// An ordinary artifact degrades unpersisted; a plan cannot. Returning one the database\n\t\t// refused would let the user approve a plan that disappears on reload.\n\t\tif (input.role === 'plan' && outcome !== 'saved') throw new ArtifactPersistenceError()\n\t\tconst written = artifact ?? draft(id)\n\t\tthis.#reflect(written)\n\t\treturn written\n\t}\n\n\t/**\n\t * Merge changes into an existing artifact. Returns undefined if `id` is unknown, or if\n\t * `opts.sessionId` is given and the artifact belongs to a different session.\n\t */\n\tasync update(\n\t\tid: string,\n\t\tinput: UpdateArtifactInput,\n\t\topts?: { sessionId?: string; canWritePlan?: () => boolean }\n\t): Promise<PersistedArtifact | undefined> {\n\t\tlet refused = false\n\t\tconst { outcome, artifact } = await mutateArtifact(id, (stored) => {\n\t\t\t// Read inside the mutator: hoisted out, it would weigh a stale copy against a fresh one.\n\t\t\tconst held = this.artifacts.find((a) => a.id === id)","sourceCodeStart":165,"sourceCodeEnd":201,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts#L165-L201","documentation":"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.","triggerScenarios":"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'.","commonSituations":"Private-browsing storage restrictions; IndexedDB quota exceeded by large plan documents; browser killing the DB mid-session; extensions blocking storage writes.","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"],"exampleFix":"// before\nconst plan = await artifacts.create({ role: 'plan', sessionId, content })\n// after\nlet plan\ntry { plan = await artifacts.create({ role: 'plan', sessionId, content }) }\ncatch (e) {\n  if (e instanceof ArtifactPersistenceError) { notifyPlanNotSaved(); return }\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"const db = await getDB()\nif (!db) throw new Error('Cannot create plan: artifact store unavailable')","typeGuard":"function isArtifactPersistenceError(e: unknown): e is ArtifactPersistenceError {\n  return e instanceof ArtifactPersistenceError\n}","tryCatchPattern":"try {\n  await artifacts.create({ role: 'plan', sessionId, content })\n} catch (e) {\n  if (e instanceof ArtifactPersistenceError) {\n    toolCallbacks.report('Plan could not be saved; do not ask the user to approve it')\n    return\n  }\n  throw e\n}","preventionTips":["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"],"tags":["copilot","artifacts","persistence","indexeddb"],"backgroundTag":"storage-unavailable","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"}