Multi-agent audit + aggressive cleanup across server/web/coder/booterm, delivered behind a DEFER discipline so none of the in-flight files were touched. Removes dead code/deps/columns, dedups server + coder helpers, and splits the oversized modules (tools.ts, opencode-server.ts, sentinel-summaries, turn.ts, TerminalPane.tsx) behind stable contracts. Adds 78 parity/unit tests (server 587, coder 323); fixes two latent bugs (ChatPane queue keys, FileViewerOverlay blank-line parity). Intended tag: v2.7.12-audit-cleanup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
import type { InferenceContext } from './inference/index.js';
|
||
import { taskModelCompletion } from './task-model.js';
|
||
|
||
const NAMING_SYSTEM_PROMPT =
|
||
'You name chat sessions. Reply with ONLY the title. 4 to 6 words. No quotes, no punctuation, no prefix.';
|
||
|
||
const MAX_TITLE_CHARS = 80;
|
||
|
||
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;
|
||
}
|
||
|
||
export async function maybeAutoNameChat(
|
||
ctx: InferenceContext,
|
||
chatId: string,
|
||
sessionId: string
|
||
): Promise<void> {
|
||
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; model: string | null }[]
|
||
>`
|
||
SELECT c.id, c.name, c.session_id, s.model
|
||
FROM chats c JOIN sessions s ON s.id = c.session_id
|
||
WHERE c.id = ${chatId}
|
||
`;
|
||
const chat = chatRows[0];
|
||
if (!chat) return;
|
||
if (chat.name !== null && chat.name !== '') return;
|
||
|
||
const firstMsgs = await ctx.sql<{ role: string; content: string }[]>`
|
||
SELECT role, content FROM messages
|
||
WHERE chat_id = ${chatId}
|
||
AND role IN ('user', 'assistant')
|
||
AND status IN ('complete', 'ok')
|
||
AND content <> ''
|
||
ORDER BY created_at ASC
|
||
LIMIT 2
|
||
`;
|
||
const userMsg = firstMsgs.find(m => m.role === 'user');
|
||
const assistantMsg = firstMsgs.find(m => m.role === 'assistant');
|
||
if (!assistantMsg) return;
|
||
|
||
let namingInput = '';
|
||
if (userMsg) namingInput += `User: ${userMsg.content.slice(0, 1000)}\n\n`;
|
||
namingInput += `Assistant: ${assistantMsg.content.slice(0, 1000)}`;
|
||
|
||
const raw = await taskModelCompletion({
|
||
system: NAMING_SYSTEM_PROMPT,
|
||
user: namingInput,
|
||
maxTokens: 30,
|
||
temperature: 0.3,
|
||
fallbackModel: chat.model ?? undefined,
|
||
});
|
||
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');
|
||
}
|
||
}
|