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.

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)

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.