{"record":{"id":"0ad6ec844a94581d","repo":"toeverything/AFFiNE","slug":"copilot-doc-not-found","errorCode":"copilot_doc_not_found","errorMessage":"Doc ${docId} not found.","messagePattern":"Doc (.+?) not found\\.","errorType":"exception","errorClass":"CopilotDocNotFound","httpStatus":404,"severity":"warning","filePath":"packages/backend/server/src/plugins/copilot/resolver.ts","lineNumber":717,"sourceCode":"  @Mutation(() => String, {\n    description: 'Create a chat session',\n  })\n  @CallMetric('ai', 'chat_session_fork')\n  async forkCopilotSession(\n    @CurrentUser() user: CurrentUser,\n    @Args({ name: 'options', type: () => ForkChatSessionInput })\n    options: ForkChatSessionInput\n  ): Promise<string> {\n    await this.ac.user(user.id).doc(options).allowLocal().assert('Doc.Update');\n    const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`;\n    await using lock = await this.mutex.acquire(lockFlag);\n    if (!lock) {\n      throw new TooManyRequest('Server is busy');\n    }\n\n    if (options.workspaceId === options.docId) {\n      // filter out session create request for root doc\n      throw new CopilotDocNotFound({ docId: options.docId });\n    }\n\n    return await this.chatSession.fork({\n      ...options,\n      userId: user.id,\n    });\n  }\n\n  @Mutation(() => [String], {\n    description: 'Cleanup sessions',\n  })\n  @CallMetric('ai', 'chat_session_cleanup')\n  async cleanupCopilotSession(\n    @CurrentUser() user: CurrentUser,\n    @Args({ name: 'options', type: () => DeleteSessionInput })\n    options: DeleteSessionInput\n  ): Promise<string[]> {\n    const { workspaceId, docId, sessionIds } = options;","sourceCodeStart":699,"sourceCodeEnd":735,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/copilot/resolver.ts#L699-L735","documentation":"forkCopilotSession rejects the request when options.docId === options.workspaceId: in AFFiNE the workspace root doc shares the workspace's id, and chat sessions cannot be forked onto the root doc, so the guard throws CopilotDocNotFound (code `copilot_doc_not_found`, status `resource_not_found`) with the offending docId. It is an input-shape rejection, not a missing document.","triggerScenarios":"Calling forkCopilotSession with the workspace id passed as docId — typically a client that defaults docId to workspace.id, spreads the wrong object into ForkChatSessionInput, or stores the root doc id where a child doc id is expected.","commonSituations":"Forking from a workspace-level context where no doc is open and the client fills docId with workspace.id; refactors that confuse `workspaceId`/`docId` in fork options; copied code from a doc-level fork used on the workspace root.","solutions":["Pass the real child doc id as docId, or omit docId when forking at workspace level (check ForkChatSessionInput optionality)","Guard before the call: if (options.docId === options.workspaceId) fix the payload instead of sending it","Log the fork payload at the call site to confirm which field carries the workspace id"],"exampleFix":"// before\nforkCopilotSession({\n  variables: { options: { sessionId, workspaceId, docId: workspaceId } }, // root doc id\n});\n\n// after\nforkCopilotSession({\n  variables: { options: { sessionId, workspaceId, docId: realDocId } }, // child doc id\n});","handlingStrategy":"validation","validationCode":"// guard: reject the root-doc misuse before sending\nfunction isForkInputValid(o: ForkChatSessionInput): boolean {\n  return o.docId == null || (o.docId !== o.workspaceId && /^[0-9a-f-]{36}$/i.test(o.docId));\n}\nif (!isForkInputValid(options)) throw new Error('docId must be a child doc, not the workspace root');","typeGuard":"function isCopilotDocNotFound(e: unknown): boolean {\n  return (e as { extensions?: { code?: string } })?.extensions?.code === 'copilot_doc_not_found';\n}","tryCatchPattern":"try {\n  await forkCopilotSession({ variables: { options } });\n} catch (e) {\n  if (isCopilotDocNotFound(e)) {\n    // payload sent the workspace id as docId; rebuild options with the real doc id\n    showPickDocDialog();\n  } else throw e;\n}","preventionTips":["Type fork options explicitly and never default docId to workspace.id","When forking from workspace context, require an explicit doc selection","Unit-test the fork payload builder for the workspace-root case"],"tags":["copilot","fork","doc","input-validation","graphql"],"backgroundTag":"invalid-resource-id","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}