diff --git a/packages/@n8n/agents/src/__tests__/title-generation.test.ts b/packages/@n8n/agents/src/__tests__/title-generation.test.ts index 7c00431d9e670..f575b67b7ca03 100644 --- a/packages/@n8n/agents/src/__tests__/title-generation.test.ts +++ b/packages/@n8n/agents/src/__tests__/title-generation.test.ts @@ -31,15 +31,15 @@ describe('generateTitleFromMessage', () => { expect(mockGenerateText).not.toHaveBeenCalled(); }); - it('returns the message itself for trivial greetings without calling the LLM', async () => { + it('returns null for trivial greetings without calling the LLM', async () => { const result = await generateTitleFromMessage(fakeModel, 'hey'); - expect(result).toBe('hey'); + expect(result).toBeNull(); expect(mockGenerateText).not.toHaveBeenCalled(); }); - it('skips the LLM for short multi-word messages', async () => { + it('returns null for short multi-word messages without calling the LLM', async () => { const result = await generateTitleFromMessage(fakeModel, 'hi there'); - expect(result).toBe('hi there'); + expect(result).toBeNull(); expect(mockGenerateText).not.toHaveBeenCalled(); }); diff --git a/packages/@n8n/agents/src/runtime/title-generation.ts b/packages/@n8n/agents/src/runtime/title-generation.ts index ff0597cb56b61..7aa5480d86de2 100644 --- a/packages/@n8n/agents/src/runtime/title-generation.ts +++ b/packages/@n8n/agents/src/runtime/title-generation.ts @@ -22,10 +22,10 @@ const TRIVIAL_MESSAGE_MAX_WORDS = 3; const MAX_TITLE_LENGTH = 80; /** - * Whether a user message is too trivial to bother sending to an LLM for - * title generation (e.g. "hey", "hello"). For these, the LLM tends to - * hallucinate an assistant-voice reply as the title instead of echoing - * the user intent — it's better to just use the message itself. + * Whether a user message has too little substance to title a conversation + * (e.g. "hey", "hello"). For these, the LLM tends to hallucinate an + * assistant-voice reply as the title — better to signal "defer, not enough + * signal yet" so the caller can retry once more context accumulates. */ function isTrivialMessage(message: string): boolean { const normalized = message.trim(); @@ -69,7 +69,7 @@ export async function generateTitleFromMessage( if (!trimmed) return null; if (isTrivialMessage(trimmed)) { - return sanitizeTitle(trimmed) || null; + return null; } const result = await generateText({ diff --git a/packages/cli/src/modules/instance-ai/instance-ai.service.ts b/packages/cli/src/modules/instance-ai/instance-ai.service.ts index 8512eacfa1e6e..d9ac7c9e446df 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai.service.ts @@ -2457,14 +2457,14 @@ export class InstanceAiService { // Skip if thread already has an LLM-refined title if (thread.metadata?.titleRefined) return; - // Get first user message + // Concat all recalled user messages so retries after a trivial first message + // (e.g. "hey") have enough signal to produce a good title. const result = await memory.recall({ threadId, resourceId: userId, perPage: 5 }); - const firstUserMsg = result.messages.find((m) => m.role === 'user'); - if (!firstUserMsg) return; - const userText = - typeof firstUserMsg.content === 'string' - ? firstUserMsg.content - : JSON.stringify(firstUserMsg.content); + const userTexts = result.messages + .filter((m) => m.role === 'user') + .map((m) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content))); + if (userTexts.length === 0) return; + const userText = userTexts.join('\n'); const llmTitle = await generateTitleForRun(modelId, userText); if (!llmTitle) return;