{"record":{"id":"2ff71815975799c0","repo":"toeverything/AFFiNE","slug":"copilot-session-not-found-2ff718","errorCode":"copilot_session_not_found","errorMessage":"Copilot session not found.","messagePattern":"Copilot session not found\\.","errorType":"exception","errorClass":"CopilotSessionNotFound","httpStatus":404,"severity":"error","filePath":"packages/backend/server/src/plugins/copilot/runtime/hosts/conversation-host.ts","lineNumber":343,"sourceCode":"      sessionId,\n      turnId: turn.id ?? '',\n    });\n    session.pushPersistedTurn(turn);\n    return {\n      turn,\n      quotaBackedRoutesAllowed: quotaAllowed,\n    };\n  }\n\n  async prepareTurn(\n    userId: string,\n    sessionId: string,\n    query: Record<string, string | string[]>\n  ): Promise<PreparedConversationTurn> {\n    const { messageId, retry, params } = ChatQuerySchema.parse(query);\n    const session = await this.sessions.get(sessionId);\n    if (!session || session.config.userId !== userId) {\n      throw new CopilotSessionNotFound();\n    }\n    const appended = await this.appendSessionMessage(\n      userId,\n      session,\n      sessionId,\n      messageId,\n      retry\n    );\n    const currentUserMessage =\n      session.stashTurns.findLast(turn => turn.role === 'user') ??\n      appended.turn;\n\n    return {\n      messageId,\n      params,\n      session,\n      latestTurn: currentUserMessage,\n      quotaBackedRoutesAllowed: appended.quotaBackedRoutesAllowed,","sourceCodeStart":325,"sourceCodeEnd":361,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/copilot/runtime/hosts/conversation-host.ts#L325-L361","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["List the user's copilot sessions first and only stream ids that appear there","On this 404, create a new session and re-send the message instead of retrying the dead id","Verify the authenticated user matches the account that owns the conversation (check token/user id)"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"// guard: only stream sessions the user owns and that still exist\nconst sessions = await fetchCopilotSessions(userId, workspaceId);\nconst mine = sessions.find(s => s.id === sessionId);\nif (!mine) {\n  sessionId = (await createCopilotSession({ variables: { options } })).data!; // fresh session\n}\nawait prepareTurn(userId, sessionId, query);","typeGuard":"function isSessionNotFound(e: unknown): boolean {\n  return (e as { extensions?: { code?: string } })?.extensions?.code === 'copilot_session_not_found';\n}","tryCatchPattern":"try {\n  await prepareTurn(userId, sessionId, query);\n} catch (e) {\n  if (isSessionNotFound(e)) {\n    const { data } = await createCopilotSession({ variables: { options } }); // fallback: recreate\n    return prepareTurn(userId, data, query);\n  }\n  throw e;\n}","preventionTips":["Open streams only for ids present in the freshly fetched session list","Handle shared conversation links by validating the session against the current user first","Ensure the auth token belongs to the account that owns the session"],"tags":["copilot","chat-session","not-found","ownership","streaming"],"backgroundTag":"session-not-found","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}