You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Starting a thread with a slash command or skill leaves the thread with no generated title. The sidebar then shows the raw prompt text, so a project's thread list fills with rows reading /sync-repo, /weekly-report, /resume-thread thr_0123456789. I expected the same title inference that runs for an ordinary prompt: the skill names how the work is done, and the rest of the message says what the work is.
The cause is the five-word eligibility gate counting the /skill-name token as an ordinary word. A skill invocation is short by design, so it lands under the threshold and inference is skipped entirely — this is not a bad title, it is no attempt at one.
Provider-independent: the path runs on the server before any provider is involved. Untitled command-first threads in the database span claude-code (16), codex (2) and an ACP agent (2).
Steps to reproduce
Open any project and start a new thread.
As the first message, invoke a skill and submit — type /<some-skill>, pick it from the typeahead so it becomes a command mention, and send.
Wait for provisioning to finish.
Look at the thread row in the sidebar, or run:
sqlite3 ~/.bb/bb.db \
"SELECT id, title, title_fallback FROM threads ORDER BY created_at DESC LIMIT 1;"
The smallest reproduction is a bare /<some-skill> with nothing after it. Adding a few words after the skill still reproduces. It stops reproducing once the message reaches five whitespace-separated tokens including the command itself, which is what makes the gate visible.
Expected vs actual
Actual — title stays NULL, and the provisioning transcript records that generation was skipped. Three threads, with the skill names replaced by placeholders:
{"type":"step","key":"metadata-completed","text":"No title generated",
"status":"completed","metadata":{"durationMs":0,"titleGenerated":false,"reason":"too-short"}}
getThreadDisplayTitle then falls through to titleFallback, which is the raw prompt text, so the row reads /skill-a.
Expected — a generated title describing the task, e.g. "Drop stale release branches".
Evidence
Direct cause, at the base commit:
shouldGenerateThreadTitle counts words from cleanPromptText, which reads part.text and ignores part.mentions entirely. The /sync-repo token is counted like any other word.
getThreadDisplayTitle falls back to titleFallback, which is why the raw slash text is what the user sees.
The stored prompt keeps the structure that would let the server tell a command from prose — it is simply not read:
{"input":[{"type":"text","text":"/sync-repo and drop the stale release branches",
"mentions":[{"start":0,"end":10,"resource":{"kind":"command","trigger":"/",
"name":"sync-repo","source":"skill","origin":"user","label":"sync-repo","argumentHint":null}}]}]}
Cross-tabulating every thread in a personal ~/.bb/bb.db (1265 threads with a prompt fallback), by whether the first token is a /command and whether the prompt is under five words:
first message
<5 words
threads
untitled
command / skill
yes
19
19 (100%)
command / skill
no
13
1
plain text
yes
200
123
plain text
no
1033
72
Thread ids and the verbatim prompts are withheld as they name private skills; they can be supplied to a maintainer on request.
What you ruled out
Not intentional. The gate landed in 6a4b7489e (2026-04-10); command/skill mentions in prompts landed in 27361799e (2026-06-15, Fix provider skill discovery typeahead #92). The gate predates the feature it now blocks, so it never made a decision about skills.
Not an inference failure. The transcript reason is too-short, and durationMs is 0 — no request is made. A separate, unrelated failed mode exists in the same data (28 of 106 recorded outcomes) where the configured fallback model rejects the request; that is a different problem and is not what this issue is about.
Not a UI bug.title is NULL in the database; the sidebar renders the documented fallback correctly.
Provider-independent. The skip happens in ensureThreadProvisionEnvironmentReady before a provider is contacted, and the affected threads span three providers.
Why this belongs in core rather than a plugin
Worth stating, since the natural question is whether a plugin could paper over it:
There is no plugin surface for thread-title policy. The available levers are experimental_hooks.on("message.dispatch") and events.on("thread.created") plus sdk.threads.update({ title }).
A plugin cannot reuse the user's configured helper inference. experimental_aiServices is registration-only — it lets a plugin serve inference to core, not consume it (this is what Expose structured BB_INFERENCE calls to server plugins #3058 asks for). A plugin would have to bring its own credentials to produce a title.
message.dispatch is fail-closed with a 10-second decision box and runs on every message; adding a titling side effect there risks failing sends for a cosmetic feature.
Either way the plugin would have to duplicate core's eligibility rule and race applyGeneratedThreadTitle, which refuses to overwrite a title that already exists.
Per AGENTS.md the server owns thread behavior and defaults, and shouldGenerateThreadTitle is the single source of truth for this rule.
Proposed fix
Exempt a prompt that carries a command mention from the length gate — invoking a skill is itself a statement of intent, whatever the word count.
Remove the command mention ranges from the text handed to the model, and pass the invoked command names as a separate template variable, so the model titles the work rather than the tool. The metadata template already asks for exactly this ("it is the problem that should be the title, not the tools"); it just never receives the information to act on.
Leave deriveTitleFallback on the raw text, so a failed inference still degrades to today's behavior.
Suggested priority and effort
Small effort, contained to apps/server/src/services/threads/title-generation.ts and the metadata template. It hits anyone who drives bb through skills — for that workflow every thread in the sidebar is unlabeled, which is a real navigation cost. There is no workaround short of renaming each thread by hand. No data is lost.
Summary
Starting a thread with a slash command or skill leaves the thread with no generated title. The sidebar then shows the raw prompt text, so a project's thread list fills with rows reading
/sync-repo,/weekly-report,/resume-thread thr_0123456789. I expected the same title inference that runs for an ordinary prompt: the skill names how the work is done, and the rest of the message says what the work is.The cause is the five-word eligibility gate counting the
/skill-nametoken as an ordinary word. A skill invocation is short by design, so it lands under the threshold and inference is skipped entirely — this is not a bad title, it is no attempt at one.Versions and environment
~/.bb/.7bbff004b("Drop the Codex copy-feedback test (Drop the Codex copy-feedback test #3885)").claude-code(16),codex(2) and an ACP agent (2).Steps to reproduce
/<some-skill>, pick it from the typeahead so it becomes a command mention, and send.The smallest reproduction is a bare
/<some-skill>with nothing after it. Adding a few words after the skill still reproduces. It stops reproducing once the message reaches five whitespace-separated tokens including the command itself, which is what makes the gate visible.Expected vs actual
Actual —
titlestaysNULL, and the provisioning transcript records that generation was skipped. Three threads, with the skill names replaced by placeholders:{"type":"step","key":"metadata-completed","text":"No title generated", "status":"completed","metadata":{"durationMs":0,"titleGenerated":false,"reason":"too-short"}}getThreadDisplayTitlethen falls through totitleFallback, which is the raw prompt text, so the row reads/skill-a.Expected — a generated title describing the task, e.g. "Drop stale release branches".
Evidence
Direct cause, at the base commit:
shouldGenerateThreadTitlecounts words fromcleanPromptText, which readspart.textand ignorespart.mentionsentirely. The/sync-repotoken is counted like any other word.getThreadDisplayTitlefalls back totitleFallback, which is why the raw slash text is what the user sees.The stored prompt keeps the structure that would let the server tell a command from prose — it is simply not read:
{"input":[{"type":"text","text":"/sync-repo and drop the stale release branches", "mentions":[{"start":0,"end":10,"resource":{"kind":"command","trigger":"/", "name":"sync-repo","source":"skill","origin":"user","label":"sync-repo","argumentHint":null}}]}]}Cross-tabulating every thread in a personal
~/.bb/bb.db(1265 threads with a prompt fallback), by whether the first token is a/commandand whether the prompt is under five words:Thread ids and the verbatim prompts are withheld as they name private skills; they can be supplied to a maintainer on request.
What you ruled out
6a4b7489e(2026-04-10); command/skill mentions in prompts landed in27361799e(2026-06-15, Fix provider skill discovery typeahead #92). The gate predates the feature it now blocks, so it never made a decision about skills.too-short, anddurationMsis0— no request is made. A separate, unrelatedfailedmode exists in the same data (28 of 106 recorded outcomes) where the configured fallback model rejects the request; that is a different problem and is not what this issue is about.titleisNULLin the database; the sidebar renders the documented fallback correctly.ensureThreadProvisionEnvironmentReadybefore a provider is contacted, and the affected threads span three providers.Why this belongs in core rather than a plugin
Worth stating, since the natural question is whether a plugin could paper over it:
experimental_hooks.on("message.dispatch")andevents.on("thread.created")plussdk.threads.update({ title }).experimental_aiServicesis registration-only — it lets a plugin serve inference to core, not consume it (this is what Expose structured BB_INFERENCE calls to server plugins #3058 asks for). A plugin would have to bring its own credentials to produce a title.message.dispatchis fail-closed with a 10-second decision box and runs on every message; adding a titling side effect there risks failing sends for a cosmetic feature.applyGeneratedThreadTitle, which refuses to overwrite a title that already exists.Per AGENTS.md the server owns thread behavior and defaults, and
shouldGenerateThreadTitleis the single source of truth for this rule.Proposed fix
deriveTitleFallbackon the raw text, so a failed inference still degrades to today's behavior.Suggested priority and effort
Small effort, contained to
apps/server/src/services/threads/title-generation.tsand the metadata template. It hits anyone who drives bb through skills — for that workflow every thread in the sidebar is unlabeled, which is a real navigation cost. There is no workaround short of renaming each thread by hand. No data is lost.