toeverything/AFFiNE · error · CopilotSessionDeleted

copilot_session_deleted

copilot_session_deleted

Error message

Copilot session has been deleted.

What it means

CopilotSessionDeleted, thrown by CopilotSessionModel.find(). find() looks for an aiSession matching userId/workspaceId/docId with parentSessionId null and no action prompt, deliberately selecting deletedAt. When a match exists but deletedAt is set (the row was soft-deleted - cleanup() sets deletedAt = new Date()), the error is thrown so the reuse path in create(state, reuseChat = true) cannot resurrect a deleted conversation.

Solutions

  1. Catch CopilotSessionDeleted and call create(state, false) to start a fresh session
  2. Refresh the session list after cleanup/delete actions so the client stops asking to reuse deleted sessions
  3. If the soft-deleted row is stale data, clear deletedAt (or hard-delete the row) in ai_session

Example fix

// before
const id = await sessions.create(state, true); // throws copilot_session_deleted

// after
let id: string;
try {
  id = await sessions.create(state, true);
} catch (e) {
  if (e instanceof CopilotSessionDeleted) id = await sessions.create(state, false);
  else throw e;
}
Defensive patterns

Strategy: try-catch

Try / catch

let sessionId: string;
try {
  sessionId = await sessions.create(state, true);
} catch (e) {
  if (e instanceof CopilotSessionDeleted) {
    sessionId = await sessions.create(state, false); // fall back to a fresh session
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: create(state, true) reaches find(state) and the matching aiSession row has a non-null deletedAt - e.g. the user previously cleaned up the chat for that doc and the client retries 'continue chat' with the reuse flag.

Common situations: Stale client state after the user deletes a conversation; cleanup jobs soft-deleting sessions while cached UI still points at them; retrying a create-with-reuse call after the earlier session was removed.

Related errors


AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18). Data as JSON: /api/errors/f1baf6b6ada5ef3d. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/models/copilot-session.ts:529

    const extraCondition: Record<string, any> = {};
    if (state.parentSessionId) {
      // also check session id if provided session is forked session
      extraCondition.id = state.sessionId;
      extraCondition.parentSessionId = state.parentSessionId;
    }

    const session = await this.db.aiSession.findFirst({
      where: {
        userId: state.userId,
        workspaceId: state.workspaceId,
        docId: state.docId,
        parentSessionId: null,
        ...this.noActionPromptCondition(),
        ...extraCondition,
      },
      select: { id: true, deletedAt: true },
    });
    if (session?.deletedAt) throw new CopilotSessionDeleted();
    return session?.id;
  }

  @Transactional()
  async getExists<Select extends Prisma.AiSessionSelect>(
    sessionId: string,
    select?: Select,
    where?: Omit<Prisma.AiSessionWhereInput, 'id' | 'deletedAt'>
  ) {
    return (await this.db.aiSession.findUnique({
      where: { ...where, id: sessionId, deletedAt: null },
      select,
    })) as Prisma.AiSessionGetPayload<{ select: Select }> | null;
  }

  @Transactional()
  async get(
    sessionId: string,

View on GitHub (pinned to 2af30773ae)