{"record":{"id":"df48a93dd90a180d","repo":"windmill-labs/windmill","slug":"the-session-plan-document-may-not-be-written-here","errorCode":null,"errorMessage":"The session plan document may not be written here","messagePattern":"The session plan document may not be written here","errorType":"exception","errorClass":"PlanWriteRefusedError","httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts","lineNumber":175,"sourceCode":"\t\t\tsessionId,\n\t\t\tchatId: input.chatId,\n\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(","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts#L157-L193","documentation":"PlanWriteRefusedError is thrown by artifactsState.create when a plan-role artifact write is attempted while the caller-provided canWritePlan() policy returns false. It distinguishes 'the plan is read-only in this posture' from 'no such artifact' (undefined), so callers cannot misread a refusal as a missing document. Tool wrappers report it back to the model as a refused plan write.","triggerScenarios":"Calling create({ role: 'plan', sessionId, ... }) when opts.canWritePlan() is false — e.g. the plan was already approved/locked, the chat is in a read-only review posture, or plan writing is disabled for the current tool context.","commonSituations":"Model trying to mint a second plan after approval; a tool invoking plan creation outside the phase where plan writes are permitted; replaying an old plan-create tool call against a session that has moved past the planning stage.","solutions":["Check canWritePlan() (or the session's plan posture) before issuing the plan-create tool call","If a plan already exists and is locked, revise the existing plan artifact (update) instead of creating a new one","Move the session back to a writable planning posture if the workflow intends a new plan"],"exampleFix":"// before\nawait artifacts.create({ role: 'plan', sessionId, content }) // throws if refused\n// after\nif (artifacts.canWritePlan()) await artifacts.create({ role: 'plan', sessionId, content })\nelse await artifacts.update(planArtifactId(sessionId), { content })","handlingStrategy":"type-guard","validationCode":"if (!canWritePlan()) {\n  throw new Error('Plan writes are not permitted in the current posture')\n}","typeGuard":"function isPlanWriteRefused(e: unknown): e is PlanWriteRefusedError {\n  return e instanceof PlanWriteRefusedError\n}","tryCatchPattern":"try {\n  await artifacts.create({ role: 'plan', sessionId, content })\n} catch (e) {\n  if (e instanceof PlanWriteRefusedError) {\n    toolCallbacks.report('Plan is read-only here; revise the existing plan instead')\n    return\n  }\n  throw e\n}","preventionTips":["Gate plan-create tool availability on the session's planning posture","Use update on the existing plan artifact once planning is locked","Report PlanWriteRefusedError to the model as a refusal, not a missing-file error"],"tags":["copilot","artifacts","policy"],"backgroundTag":"write-policy-refused","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"}