janhq/jan · error
This conversation has no user message to respond to. Add a…
Error message
This conversation has no user message to respond to. Add a message, or regenerate from a turn that includes your question.
What it means
Thrown by assertSendable in custom-chat-transport.ts when the message window handed to a chat template contains no genuine user query. Many chat templates (Qwen 3.5+) throw a cryptic Jinja rendering error when asked to generate without a user turn, so the transport fails early with an actionable message. It protects against windows that were reduced to only assistant/tool messages by deletion or context eviction.
Solutions
- Add a new user message to the conversation before sending.
- Regenerate from an earlier turn that still contains a user question.
- Adjust eviction/deletion logic so at least one user turn is always retained in the window.
- If you manage messages programmatically, check hasGenuineUserQuery(messages) before calling send.
Example fix
// before
const window = messages.filter((m) => m.id !== deletedId)
send({ messages: window })
// after
const window = messages.filter((m) => m.id !== deletedId)
if (!hasGenuineUserQuery(window)) {
throw new Error('Cannot regenerate: the pruned window has no user message')
}
send({ messages: window }) Defensive patterns
Strategy: validation
Validate before calling
function hasGenuineUserQuery(messages) {
return messages.some((m) => m.role === 'user' && typeof m.content === 'string' ? m.content.trim().length > 0 : Array.isArray(m.content) && m.content.some((p) => p.type === 'text' && p.text.trim()))
}
if (!hasGenuineUserQuery(window)) showAddMessageHint()
else send({ messages: window }) Type guard
const hasUserTurn = (ms) => ms.some((m) => m.role === 'user')
Try / catch
try { await send({ messages: window }) } catch (e) { if (String(e.message).includes('no user message to respond to')) { promptUserToAddMessage() } else throw e } Prevention
- Before regenerate, assert the window still contains a user turn.
- Keep at least one user message pinned during context eviction.
- In tests, cover delete-then-regenerate flows.
When it happens
Trigger: Regenerating from a turn where the last user message was deleted; context-window eviction trimming out all user messages; sending a conversation whose visible turns are assistant-only; programmatic message list construction omitting any user role message.
Common situations: Users delete an early question then hit regenerate; long conversations where older user turns were evicted; importing or replaying a chat log that starts with an assistant reply; agents that only append tool/assistant messages.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- ${error.message}
- Failed to parse archive name
- File ' ' exceeds size limit ( bytes > MB).
- File ' ' has already been attached to this thread
- Invalid argument
AI-assisted analysis of janhq/jan@7205d770c1 (2026-09-17).
Data as JSON: /api/errors/b5afc636cd5924ff.
Report an issue: GitHub.
Appendix: source
Thrown at web-app/src/lib/custom-chat-transport.ts:957
this.systemMessage,
this.buildMemorySystemInstruction(),
this.buildFilesSystemInstruction(messages),
this.buildWebSearchSystemInstruction(),
this.buildAgentToolsSystemInstruction(),
]
.filter((s) => typeof s === 'string' && s.trim().length > 0)
.join('\n\n') || undefined
return typeof raw === 'string' && raw.trim().length > 0 ? raw : undefined
}
/**
* Many chat templates (Qwen3.5+) reject a window with no genuine user query
* and throw a cryptic Jinja error. Fail early with a clear message when
* deletion/eviction has left no real user turn to respond to.
*/
protected assertSendable(messages: UIMessage[]): void {
if (!hasGenuineUserQuery(messages)) {
throw new Error(
'This conversation has no user message to respond to. Add a message, or regenerate from a turn that includes your question.'
)
}
}
async refreshTools(abortSignal?: AbortSignal) {
// Resolve the memory digest before the prompt is built: the builder is
// sync, so it reads the snapshot this await guarantees. Error-safe and
// cached inside agentTools, so this is one IPC round-trip per store change.
await refreshMemoryDigest()
if (!this.serviceHub) {
this.tools = {}
return
}
const toolsRecord: Record<string, Tool> = {}
// Tool availability is global (shared across all chats).View on GitHub (pinned to 7205d770c1)