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
- Strip the action (send action: '' or omit it) when the session has no docId or is pinned
- Attach the session to a doc (pass docId) so getSessionType returns Doc and action prompts are allowed
- 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
- Derive the session type (pinned > no-docId > doc) before applying a prompt
- Default action to null in shared prompt payloads and set it only for doc chats
- Hide action prompts in workspace and pinned chat UIs
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
- copilot_session_invalid_input
- action_bridge_stream_error
- copilot_prompt_invalid
- copilot_session_deleted
- copilot_session_not_found
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)