{"record":{"id":"9b5de2cfb7c9a6d8","repo":"windmill-labs/windmill","slug":"planslottakenerror","errorCode":null,"errorMessage":"PlanSlotTakenError","messagePattern":"PlanSlotTakenError","errorType":"exception","errorClass":"PlanSlotTakenError","httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts","lineNumber":177,"sourceCode":"\t\t\tkind: input.kind ?? 'md',\n\t\t\trole: input.role,\n\t\t\tapprovedVersion: input.approvedVersion,\n\t\t\tname: input.name,\n\t\t\tcontent: input.content,\n\t\t\tcreatedAt: now,\n\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,","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts#L159-L195","documentation":"PlanSlotTakenError is thrown by artifactsState.create when a plan-role create targets a session that already has a plan artifact. A session holds exactly one plan: the id is deterministic (planArtifactId(sessionId)), so a second create collides with the existing document inside mutateArtifact, and the error carries that existing plan because the useful next step is to revise it.","triggerScenarios":"Calling create({ role: 'plan', sessionId, ... }) when a persisted plan artifact already exists for that session — duplicate plan creation by the model, or a race where two plan writes run concurrently.","commonSituations":"Model re-issuing a plan-create tool call after a retry that actually succeeded; a workflow stage resetting without clearing the old plan; the user re-running the planning step on a session that already stored a plan.","solutions":["Catch PlanSlotTakenError and use error.plan (or planArtifactId(sessionId)) to update/revise the existing plan instead of creating one","Read the existing plan first (getVersion/getArtifactVersion) and only create when none exists","Clear/replace the session's plan through the update path so approval state carries over correctly"],"exampleFix":"// before\nawait artifacts.create({ role: 'plan', sessionId, content }) // throws when slot taken\n// after\ntry { await artifacts.create({ role: 'plan', sessionId, content }) }\ncatch (e) {\n  if (e instanceof PlanSlotTakenError) await artifacts.update(e.plan.id, { content })\n  else throw e\n}","handlingStrategy":"try-catch","validationCode":"const existingPlan = await getArtifactVersion(planArtifactId(sessionId), 1)\nif (existingPlan) throw new Error('Plan already exists; revise it via update')","typeGuard":"function isPlanSlotTaken(e: unknown): e is PlanSlotTakenError {\n  return e instanceof PlanSlotTakenError\n}","tryCatchPattern":"try {\n  await artifacts.create({ role: 'plan', sessionId, content })\n} catch (e) {\n  if (e instanceof PlanSlotTakenError) {\n    await artifacts.update(e.plan.id, { content })\n    return\n  }\n  throw e\n}","preventionTips":["Deduplicate model tool calls so a successful plan create is never retried","Because the plan id is deterministic per session, prefer create-if-absent/update-if-present logic","Carry approval state via keepApproved/approvedVersion when replacing plans"],"tags":["copilot","artifacts","duplicate-key"],"backgroundTag":"duplicate-resource-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"}