toeverything/AFFiNE · error · CopilotSessionNotFound

copilot_session_not_found

copilot_session_not_found

Error message

Copilot session not found.

What it means

prepareTurn (the entry point for preparing a conversation turn/stream) loads the session and requires session.config.userId === userId; when the session is missing or owned by a different user it throws CopilotSessionNotFound (code `copilot_session_not_found`, status `resource_not_found`). Ownership is enforced before any message handling.

Source

Thrown at packages/backend/server/src/plugins/copilot/runtime/hosts/conversation-host.ts:343

      sessionId,
      turnId: turn.id ?? '',
    });
    session.pushPersistedTurn(turn);
    return {
      turn,
      quotaBackedRoutesAllowed: quotaAllowed,
    };
  }

  async prepareTurn(
    userId: string,
    sessionId: string,
    query: Record<string, string | string[]>
  ): Promise<PreparedConversationTurn> {
    const { messageId, retry, params } = ChatQuerySchema.parse(query);
    const session = await this.sessions.get(sessionId);
    if (!session || session.config.userId !== userId) {
      throw new CopilotSessionNotFound();
    }
    const appended = await this.appendSessionMessage(
      userId,
      session,
      sessionId,
      messageId,
      retry
    );
    const currentUserMessage =
      session.stashTurns.findLast(turn => turn.role === 'user') ??
      appended.turn;

    return {
      messageId,
      params,
      session,
      latestTurn: currentUserMessage,
      quotaBackedRoutesAllowed: appended.quotaBackedRoutesAllowed,

View on GitHub (pinned to b4c8548c09)

Solutions

  1. List the user's copilot sessions first and only stream ids that appear there
  2. On this 404, create a new session and re-send the message instead of retrying the dead id
  3. Verify the authenticated user matches the account that owns the conversation (check token/user id)
Defensive patterns

Strategy: fallback

Validate before calling

// guard: only stream sessions the user owns and that still exist
const sessions = await fetchCopilotSessions(userId, workspaceId);
const mine = sessions.find(s => s.id === sessionId);
if (!mine) {
  sessionId = (await createCopilotSession({ variables: { options } })).data!; // fresh session
}
await prepareTurn(userId, sessionId, query);

Type guard

function isSessionNotFound(e: unknown): boolean {
  return (e as { extensions?: { code?: string } })?.extensions?.code === 'copilot_session_not_found';
}

Try / catch

try {
  await prepareTurn(userId, sessionId, query);
} catch (e) {
  if (isSessionNotFound(e)) {
    const { data } = await createCopilotSession({ variables: { options } }); // fallback: recreate
    return prepareTurn(userId, data, query);
  }
  throw e;
}

Prevention

When it happens

Trigger: Opening a chat stream with a stale sessionId (session deleted/cleaned), a session id from another user (shared/leaked link), or an auth mismatch where the token's user id differs from the session owner.

Common situations: Deep links to old conversations after cleanup; multi-account browsers sending one account's token with another account's session link; session deleted in another tab; tests with mismatched user fixtures.

Related errors


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