toeverything/AFFiNE · error · CopilotPromptInvalid

copilot_prompt_invalid

copilot_prompt_invalid

Error message

${promptName} are not allowed for ${sessionType} sessions

What it means

CopilotPromptInvalid, thrown by CopilotSessionModel.checkSessionPrompt. The session type is derived from the session shape (getSessionType at copilot-session.ts:350): pinned -> Pinned, no docId -> Workspace, otherwise Doc. Prompts carrying a non-empty action (prompt.action?.trim() is truthy) are only valid for Doc sessions, because the action binds to a specific doc's context. Applying an action prompt to a workspace-level or pinned session is rejected before the session is created or updated.

Solutions

  1. Strip the action (send action: '' or omit it) when the session has no docId or is pinned
  2. Attach the session to a doc (pass docId) so getSessionType returns Doc and action prompts are allowed
  3. Unpin the session or choose a regular doc chat session before applying the action prompt

Example fix

// before
// state has no docId -> Workspace session
await sessions.create({ ...state, prompt: { name: 'edit-existing', action: 'edit-existing' } });

// after
const type = state.pinned ? 'pinned' : state.docId ? 'doc' : 'workspace';
const prompt = type === 'doc'
  ? { name: 'edit-existing', action: 'edit-existing' }
  : { name: 'edit-existing' }; // no action outside doc sessions
await sessions.create({ ...state, prompt });
Defensive patterns

Strategy: validation

Validate before calling

function sessionTypeOf(s: { docId?: string | null; pinned?: boolean | null }) {
  if (s.pinned) return 'pinned';
  return s.docId ? 'doc' : 'workspace';
}

function promptFitsSession(
  prompt: { action?: string | null },
  session: { docId?: string | null; pinned?: boolean | null }
) {
  return sessionTypeOf(session) === 'doc' || !prompt.action?.trim();
}

if (!promptFitsSession(prompt, state)) {
  prompt = { ...prompt, action: null }; // strip the action outside doc sessions
}

Type guard

const isActionPrompt = (p: Partial<ChatPrompt>): boolean => !!p.action?.trim();

Try / catch

try {
  await sessions.create(state);
} catch (e) {
  if (e instanceof CopilotPromptInvalid) {
    // retry without the action, or inform the user action prompts need a doc session
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling a create/update flow that runs checkSessionPrompt with a session where pinned is true or docId is null/undefined, while the prompt object has a non-blank action string. The message interpolates the prompt name and the resolved session type.

Common situations: Frontend reusing a doc action prompt (e.g. an 'improve this doc' action) for the workspace-level copilot panel; applying the last-used prompt payload to a pinned session; prompt templates that default action to a stale value.

Related errors


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

Appendix: source

Thrown at packages/backend/server/src/models/copilot-session.ts:420

  getSessionType(session: Pick<ChatSession, 'docId' | 'pinned'>): SessionType {
    if (session.pinned) return SessionType.Pinned;
    if (!session.docId) return SessionType.Workspace;
    return SessionType.Doc;
  }

  checkSessionPrompt(
    session: Pick<ChatSession, 'docId' | 'pinned'>,
    prompt: Partial<ChatPrompt>
  ): boolean {
    const sessionType = this.getSessionType(session);
    const { name: promptName, action: promptAction } = prompt;

    // workspace and pinned sessions cannot use action prompts
    if (
      [SessionType.Workspace, SessionType.Pinned].includes(sessionType) &&
      !!promptAction?.trim()
    ) {
      throw new CopilotPromptInvalid(
        `${promptName} are not allowed for ${sessionType} sessions`
      );
    }

    return true;
  }

  @Transactional()
  async create(state: ChatSession, reuseChat = false): Promise<string> {
    await this.lockPersonalScope(
      state.workspaceId,
      state.userId,
      state.personal
    );
    // find and return existing session if session is chat session
    if (reuseChat && !state.promptAction) {
      const sessionId = await this.find(state);
      if (sessionId) return sessionId;

View on GitHub (pinned to 2af30773ae)