import type { InferenceContext } from './inference/index.js'; const NAMING_SYSTEM_PROMPT = 'You name chat sessions based on what the assistant did. Summarize the topic or outcome — do NOT copy the first few words verbatim. Reply directly with no thinking, reasoning, or explanation. Output ONLY the title, 4 words max, no quotes, no punctuation, no prefix like "Title:".'; const MAX_TITLE_CHARS = 60; function cleanTitle(raw: string): string { let name = raw.trim(); const quotes = ['"', "'", '`', '‘', '’', '“', '”']; while (name.length >= 2 && quotes.includes(name[0]!) && quotes.includes(name[name.length - 1]!)) { name = name.slice(1, -1).trim(); } name = name.replace(/^title\s*:\s*/i, '').trim(); if (name.length > MAX_TITLE_CHARS) { name = name.slice(0, MAX_TITLE_CHARS).trim(); } return name; } interface NamingResponse { choices?: Array<{ message?: { content?: string; reasoning_content?: string; }; }>; } function pickTitleSource(data: NamingResponse): string { const choice = data.choices?.[0]?.message; if (!choice) return ''; if (choice.content && choice.content.trim().length > 0) return choice.content; const reasoning = choice.reasoning_content ?? ''; if (reasoning.length === 0) return ''; const lines = reasoning .split('\n') .map((l) => l.trim()) .filter((l) => l.length > 0); return lines[lines.length - 1] ?? ''; } export async function maybeAutoNameChat( ctx: InferenceContext, chatId: string, sessionId: string ): Promise { const counts = await ctx.sql<{ n: number }[]>` SELECT COUNT(*)::int AS n FROM messages WHERE chat_id = ${chatId} AND role = 'assistant' AND status = 'complete' AND content <> '' `; if ((counts[0]?.n ?? 0) < 1) return; const chatRows = await ctx.sql< { id: string; name: string | null; session_id: string }[] >` SELECT id, name, session_id FROM chats WHERE id = ${chatId} `; const chat = chatRows[0]; if (!chat) return; if (chat.name !== null && chat.name !== '') return; const sessionRows = await ctx.sql<{ model: string }[]>` SELECT model FROM sessions WHERE id = ${sessionId} `; // v2.0.5: prefer FAST_MODEL for cheap LLM calls (titles, summaries). const model = ctx.config.FAST_MODEL ?? sessionRows[0]?.model; if (!model) return; const assistantMsg = await ctx.sql<{ content: string }[]>` SELECT content FROM messages WHERE chat_id = ${chatId} AND role = 'assistant' AND status = 'complete' AND content <> '' ORDER BY created_at ASC LIMIT 1 `; if (!assistantMsg[0]) return; const assistantText = assistantMsg[0].content.slice(0, 2000); const body = { model, messages: [ { role: 'system', content: NAMING_SYSTEM_PROMPT }, { role: 'user', content: assistantText, }, ], max_tokens: 30, temperature: 0.3, stream: false, chat_template_kwargs: { enable_thinking: false }, }; const res = await fetch(`${ctx.config.LLAMA_SWAP_URL}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`naming request failed: ${res.status} ${text.slice(0, 200)}`); } const data = (await res.json()) as NamingResponse; const raw = pickTitleSource(data); const name = cleanTitle(raw); if (!name) { ctx.log.warn({ chatId, raw }, 'auto-name: empty title from model'); return; } const updated = await ctx.sql<{ id: string; name: string; session_id: string; updated_at: string }[]>` UPDATE chats SET name = ${name}, updated_at = clock_timestamp() WHERE id = ${chatId} AND (name IS NULL OR name = '') RETURNING id, name, session_id, updated_at `; if (updated.length === 0) return; ctx.publish(sessionId, { type: 'chat_renamed', chat_id: chatId, name, }); ctx.publishUser({ type: 'chat_updated', chat_id: chatId, session_id: sessionId, name, updated_at: updated[0]!.updated_at, }); ctx.log.info({ chatId, name }, 'chat auto-named'); // Propagate to the parent session if it's still on its default name. // The WHERE guard makes the check atomic — if the user has already // renamed (or a prior chat already propagated), this UPDATE matches // zero rows and we do nothing. First chat wins; manual renames win. const renamedSession = await ctx.sql<{ id: string; name: string }[]>` UPDATE sessions SET name = ${name} WHERE id = ${sessionId} AND name = 'New session' RETURNING id, name `; if (renamedSession.length > 0) { ctx.publishUser({ type: 'session_renamed', session_id: sessionId, name, }); ctx.log.info({ sessionId, name }, 'session auto-named from chat'); } }