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

  1. Add a new user message to the conversation before sending.
  2. Regenerate from an earlier turn that still contains a user question.
  3. Adjust eviction/deletion logic so at least one user turn is always retained in the window.
  4. 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

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


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)