Titles #
Summary #
After the first reply in a conversation finishes, SCN Chat asks a model for a short title and writes it to the conversation's info record with titleSource: "generated". This is the first turn hook, shipped as the @scn-chat/plugin-titles plugin. It never replaces a title the user wrote, and it respects the user's generateTitles preference.
Motivation #
A chat list of "New chat" entries is useless. Automatic titles are standard in ChatGPT, Claude, and Open WebUI. Building titles as a plugin also proves the hook system works for a real feature.
Design #
Plugin #
plugins/titles exports a factory:
titles({ model?: ModelRef, maxWords?: number })
modelpicks the model used for titles, for example a cheap admin model. When it is left out, the title uses the same model and credentials that wrote the reply.maxWordsdefaults to 6.
The plugin registers one turn:after action.
When a title is generated #
The handler generates a title only when all of these are true:
- The reply's status is
complete. - The conversation's info record has no
title. info.titleSourceis not"user".- The user's preferences do not set
generateTitlestofalse. A missing preferences record counts astrue. - The conversation's content is not encrypted.
The turn context from the chat-turns spec carries the conversation info and the user's preferences, so the handler makes no extra reads.
Generating #
The handler calls ctx.models.generateText for the user. The system prompt asks for a title of at most maxWords words, with no quotes and no trailing punctuation, in the language of the conversation. The input is the text parts of the triggering user message and the reply, each cut to 2,000 characters. Reasoning, tool calls, and attachments are left out. The call sets effort to none, so a reasoning model spends its output on the title, and allows 1,000 output tokens for models that reason anyway.
The output is cleaned up before writing. Whitespace is collapsed, surrounding quotes and trailing periods are removed, and the result is cut to the lexicon's 300 graphemes. An empty result is treated as a failure.
Writing #
The handler calls ctx.conversations.updateInfo with title and titleSource: "generated". The storage layer re-reads the info record before writing. If the user has set a title in the meantime, the update is skipped.
Failure #
A failed model call or an empty result logs a warning with the conversation URI and the error, and no title is written. An empty result's error names the model's finish reason. A title is a nice-to-have, so a failure never affects the turn. The next completed reply tries again, since the conversation still has no title.
Scope Boundaries #
- No title regeneration after the first one succeeds.
- No titles for encrypted conversations.
- No UI beyond showing the title. Renaming a chat is part of the web-ui spec, and sets
titleSource: "user".
Edge Cases and Decisions #
- The default title model is the one that wrote the reply, so a BYO-key user's titles bill to their own key, and nothing breaks when no cheap admin model is configured.
- A title the user writes while generation is running wins, because the write re-checks
titleSource. - Direct-PDS clients get titles too, since the hook runs for every turn the server generates, whatever started it.