toeverything/AFFiNE · warning · CopilotDocNotFound

copilot_doc_not_found

copilot_doc_not_found

Error message

Doc ${docId} not found.

What it means

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.

Solutions

  1. Pass the real child doc id as docId, or omit docId when forking at workspace level (check ForkChatSessionInput optionality)
  2. Guard before the call: if (options.docId === options.workspaceId) fix the payload instead of sending it
  3. Log the fork payload at the call site to confirm which field carries the workspace id

Example fix

// before
forkCopilotSession({
  variables: { options: { sessionId, workspaceId, docId: workspaceId } }, // root doc id
});

// after
forkCopilotSession({
  variables: { options: { sessionId, workspaceId, docId: realDocId } }, // child doc id
});
Defensive patterns

Strategy: validation

Validate before calling

// guard: reject the root-doc misuse before sending
function isForkInputValid(o: ForkChatSessionInput): boolean {
  return o.docId == null || (o.docId !== o.workspaceId && /^[0-9a-f-]{36}$/i.test(o.docId));
}
if (!isForkInputValid(options)) throw new Error('docId must be a child doc, not the workspace root');

Type guard

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

Try / catch

try {
  await forkCopilotSession({ variables: { options } });
} catch (e) {
  if (isCopilotDocNotFound(e)) {
    // payload sent the workspace id as docId; rebuild options with the real doc id
    showPickDocDialog();
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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

Appendix: source

Thrown at packages/backend/server/src/plugins/copilot/resolver.ts:717

  @Mutation(() => String, {
    description: 'Create a chat session',
  })
  @CallMetric('ai', 'chat_session_fork')
  async forkCopilotSession(
    @CurrentUser() user: CurrentUser,
    @Args({ name: 'options', type: () => ForkChatSessionInput })
    options: ForkChatSessionInput
  ): Promise<string> {
    await this.ac.user(user.id).doc(options).allowLocal().assert('Doc.Update');
    const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`;
    await using lock = await this.mutex.acquire(lockFlag);
    if (!lock) {
      throw new TooManyRequest('Server is busy');
    }

    if (options.workspaceId === options.docId) {
      // filter out session create request for root doc
      throw new CopilotDocNotFound({ docId: options.docId });
    }

    return await this.chatSession.fork({
      ...options,
      userId: user.id,
    });
  }

  @Mutation(() => [String], {
    description: 'Cleanup sessions',
  })
  @CallMetric('ai', 'chat_session_cleanup')
  async cleanupCopilotSession(
    @CurrentUser() user: CurrentUser,
    @Args({ name: 'options', type: () => DeleteSessionInput })
    options: DeleteSessionInput
  ): Promise<string[]> {
    const { workspaceId, docId, sessionIds } = options;

View on GitHub (pinned to b4c8548c09)