{"record":{"id":"f1baf6b6ada5ef3d","repo":"toeverything/AFFiNE","slug":"copilot-session-deleted","errorCode":"copilot_session_deleted","errorMessage":"Copilot session has been deleted.","messagePattern":"Copilot session has been deleted\\.","errorType":"error_code","errorClass":"CopilotSessionDeleted","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/models/copilot-session.ts","lineNumber":472,"sourceCode":"    const extraCondition: Record<string, any> = {};\n    if (state.parentSessionId) {\n      // also check session id if provided session is forked session\n      extraCondition.id = state.sessionId;\n      extraCondition.parentSessionId = state.parentSessionId;\n    }\n\n    const session = await this.db.aiSession.findFirst({\n      where: {\n        userId: state.userId,\n        workspaceId: state.workspaceId,\n        docId: state.docId,\n        parentSessionId: null,\n        ...this.noActionPromptCondition(),\n        ...extraCondition,\n      },\n      select: { id: true, deletedAt: true },\n    });\n    if (session?.deletedAt) throw new CopilotSessionDeleted();\n    return session?.id;\n  }\n\n  @Transactional()\n  async getExists<Select extends Prisma.AiSessionSelect>(\n    sessionId: string,\n    select?: Select,\n    where?: Omit<Prisma.AiSessionWhereInput, 'id' | 'deletedAt'>\n  ) {\n    return (await this.db.aiSession.findUnique({\n      where: { ...where, id: sessionId, deletedAt: null },\n      select,\n    })) as Prisma.AiSessionGetPayload<{ select: Select }> | null;\n  }\n\n  @Transactional()\n  async get(sessionId: string) {\n    return await this.getExists(sessionId, {","sourceCodeStart":454,"sourceCodeEnd":490,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/models/copilot-session.ts#L454-L490","documentation":"CopilotSessionDeleted (action_forbidden / copilot_session_deleted) thrown by CopilotSessionModel.find (packages/backend/server/src/models/copilot-session.ts:472). find() looks up a root (parentSessionId null), no-action chat session matching userId/workspaceId/docId. If a matching row exists but its deletedAt is non-null (soft-deleted), the session cannot be reused and the guard throws.","triggerScenarios":"Calling find(state) — typically from create(state, reuseChat=true) at copilot-session.ts:380 — when the only matching session has been soft-deleted (deletedAt set). The query selects id and deletedAt; if deletedAt is truthy, reuse is refused.","commonSituations":"User deleted a chat session in the UI, then the client (still holding the prior state) tries to reuse it instead of creating a new one; race between delete and the next message send; reuseChat=true passed after a prior cleanup job soft-deleted old sessions.","solutions":["Stop reusing the deleted session: create a fresh one (reuseChat=false or clear the cached sessionId client-side).","Before reusing, check session.deletedAt via getExists and skip reuse if set.","If restoration is intended, clear deletedAt in the DB (operational step) before reusing.","Client-side: on receiving copilot_session_deleted, evict the cached sessionId and retry as a new session."],"exampleFix":"// before\nconst id = await sessionModel.create(state, /* reuseChat */ true);\n// after\nconst existing = await sessionModel.getExists(state.sessionId, { id: true, deletedAt: true });\nif (existing?.deletedAt) {\n  state.sessionId = undefined; // force new session\n}\nconst id = await sessionModel.create(state, /* reuseChat */ true);","handlingStrategy":"validation","validationCode":"const existing = await sessionModel.getExists(state.sessionId, { id: true, deletedAt: true });\nif (existing?.deletedAt) {\n  state.sessionId = undefined; // force new session\n}\nconst id = await sessionModel.create(state, true);","typeGuard":"const isReusable = (s: { id: string; deletedAt: Date | null } | null): s is { id: string; deletedAt: null } =>\n  s !== null && s.deletedAt === null;","tryCatchPattern":"try {\n  await sessionModel.create(state, true);\n} catch (e) {\n  if (e instanceof UserFriendlyError && e.code === 'copilot_session_deleted') {\n    state.sessionId = undefined;\n    await sessionModel.create(state, true);\n    return;\n  }\n  throw e;\n}","preventionTips":["Evict cached sessionIds when the user deletes a chat.","Check deletedAt before attempting reuseChat=true.","On copilot_session_deleted, recreate the session and retry."],"tags":["copilot","session","soft-delete","reuse"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}