Skip to content

A thread whose first message invokes a skill is never titled #3886

Description

@Willhong

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-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.

Versions and environment

  • bb 0.43.1, packaged desktop app, data dir ~/.bb/.
  • Investigated from source at 7bbff004b ("Drop the Codex copy-feedback test (Drop the Codex copy-feedback test #3885)").
  • macOS 26.6.2, Node v25.9.0.
  • 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

  1. Open any project and start a new thread.
  2. 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.
  3. Wait for provisioning to finish.
  4. 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

Actualtitle stays NULL, and the provisioning transcript records that generation was skipped. Three threads, with the skill names replaced by placeholders:

"/skill-a"                        → reason: "too-short"   (1 token)
"/skill-b"                        → reason: "too-short"   (1 token)
"/skill-b <three more words>"     → reason: "too-short"   (4 tokens)
{"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:

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.
  • Not a duplicate. Searched open and closed issues for "thread title", "title generation", "slash command title", "skill invocation", "too-short". The nearest neighbours are Document custom prompts for automatic thread names #1442 (documenting custom title prompts), Title Claude Code command rows with the agent's description, not the parsed command #3709 (titling Claude Code command rows, not threads) and Expose structured BB_INFERENCE calls to server plugins #3058 (exposing inference to plugins); none covers this path.
  • 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

  1. 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.
  2. 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.
  3. 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.

AGENT GENERATED

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmed-reproBug reproduced again from a clean trusted checkout; see linked reportthreadsTurns, timeline, messaging, forks

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions