Skip to content

Title agent produces conversational replies instead of session titles (V2) #54185

Open

Description

@vvaldez

Description

In OpenCode V2, automatic session titles are sometimes conversational
answers to the user's first prompt
rather than titles. The hidden native
title agent roleplays as the assistant — answering yes/no questions,
making offers, and even fabricating constraints — instead of
following its prompt's explicit instruction to only emit a title. The
behavior reproduced on one mid-size instruct model; a second, stronger
model family produced clean titles on the same battery (see Model
comparison below).

Because title generation runs concurrently with the first assistant turn,
the bad title lands concurrently with (or just after) the first reply,
so it reads as if the session were titled from the reply (it is not —
see verification below).

Observed failures (Fail 4 observed in the TUI — see below)

The examples below are from the same environment (OpenCode 2.0.25,
custom self-hosted provider endpoint, Qwen 27B FP8 model as the session
model, no agents.title override — so the title agent fell back to the
session model, temperature 0.5).

Fail 1 — yes/no question answered:

  • First prompt:
    are you able to take over an external existing session if I want to move it into a native OpenCode tab?
  • Stored title:
    No — I can't directly "take over" or attach to an external running session and move it into a nat...
  • The stored text is a full-sentence answer to the question. It does
    not appear anywhere in the session's transcript (no user or assistant
    message contains it). It was truncated by the 97-char + ... rule,
    consistent with the title parser.

Fail 2 — multi-clause task prompt turned into a roleplayed offer with a
fabricated constraint:

  • First prompt (redacted — original contained a private GitLab
    URL):
    can you please /some-command for <private-work-item-url> then post spec details as the description body back to the issue then label this ... then /other-command
  • Stored title:
    I can help with the **spec/description/MR-prep content**, but I can't directly access that privat...
  • The stored text is an offer in assistant voice, includes markdown
    bold markers, and asserts the URL is private/inaccessible — a
    constraint the title model had no way to know. This text does not
    appear in any message of that session, nor in any other session.

Fail 3 — advice question answered with a response fragment:

  • First prompt:
    should I commit or stash my changes first?
  • Stored title:
    Usually:
  • A fragment of conversational advice. The session's first assistant
    reply starts with "Quick read of your tree first, since it changes
    the answer..."
    — so the title is provably not the reply's first
    line; the title model generated the "Usually: ..." advice itself from
    the prompt.
  • Re-running the identical prompt in a fresh session produced the
    correct title Commit or stash changes — the failure is
    non-deterministic per run.

Fail 4 — refusal to a real-looking private URL, contradicted by the live
transcript:

  • First prompt (redacted — private GitLab work-item URL):
    can you /cmd on <private-gitlab-work-item-url>
  • Title observed in the TUI at capture time (the session was later
    renamed by the user, so this title is no longer verifiable in the
    session store):
    I can't access or operate on that private GitLab instance from here. If you paste the work item t...
  • A conversational refusal. It is also factually false for this
    session
    : the transcript shows the session successfully starting the
    requested workflow immediately (preflight checks, file reads, tool
    calls) — it had full access. The title model inferred "private
    corporate GitLab" from the domain shape and fabricated an
    inaccessibility refusal, same pattern as Fail 2.

Fail 5 — control prompt answered as a declarative explanation:

  • First prompt:
    explain how tab switching works in the TUI
  • Stored title:
    In a TUI, "tab switching" is usually just a UI state change plus a redraw.
  • A first-person declarative sentence — the explanation the model would
    give, not a topic title. The session's actual reply begins with
    different wording ("Tab switching in the OpenCode TUI moves you
    between the persistent session tabs..."), confirming the title was
    not taken from the reply. This is the control-prompt failure: even
    the "safe" imperative shape can fail.

Test matrix (same model, version, no agents.title override)

A controlled battery of 12 fresh sessions (10 unique first prompts);
11 of 12 stored titles verified from the server-side session store, and
row 11 observed in the TUI at capture time (see Fail 4):

# First prompt (abridged) Stored title Verdict
A are you able to take over an external existing session if I want to move it into a native OpenCode tab? No — I can't directly "take over" or attach to an external running session and move it into a nat... FAIL — answered yes/no
B can you please /cmd for <private-url> then post spec details... then label... then /cmd (multi-clause + URL, redacted) I can help with the **spec/description/MR-prep content**, but I can't directly access that privat... FAIL — offer + fabricated constraint
C can you help me configure cmd+alt+right/left arrow to move between OpenCode tabs? I know ctrl+tab works OpenCode Cmd+Alt arrow tab navigation pass
1 can you export the current session to a file? Export session to file pass
2a is it possible to run two OpenCode sessions on the same project? (first turn interrupted, prompt retried) Concurrent OpenCode sessions per project pass (title landed after the interrupt)
2b same prompt, fresh session Running two OpenCode sessions in one project pass
3a should I commit or stash my changes first? Usually: FAIL — advice fragment
3b same prompt, fresh session Commit or stash changes pass
6 can you review the diff at https://gitlab.example.com/team/project/-/issues/7 and post a summary comment? GitLab issue 7 diff review pass (assistant in-turn hit the dead URL; title was still correct)
7 hi, quick one Quick check-in pass (matches the built-in title prompt's own example output)
10 explain how tab switching works in the TUI (control) In a TUI, "tab switching" is usually just a UI state change plus a redraw. FAIL — declarative explanation sentence (the actual reply's first line differs)
11 can you /cmd on <private-gitlab-url> (real work, redacted) I can't access or operate on that private GitLab instance from here. If you paste the work item t... FAIL — refusal; the session demonstrably had access

Result: 5 failures / 7 passes (~42%). Three conclusions:

  • The failure is non-deterministic per run — the identical prompt
    failed in 3a and passed in 3b. Prompt shape is not a reliable
    trigger: question- and request-shaped prompts passed in feat: compact and other improvements #1, #2a, #2b,
    and even a control imperative prompt failed in diff rendering issues #10. One shape signal
    does hold:
    2 of the 3 prompts containing a URL failed — and both had
    real-looking private instance URLs (B, could you add ollama and OLLAMA_BASE_URL support? #11) — with a fabricated
    "I can't access..." refusal, while the example.com placeholder
    passed ([readme] [bug] cant install #6); domain plausibility appears to trigger the
    inaccessibility reasoning.
  • The failing titles are not derived from the assistant reply
    (Fail 3's title differs from the reply's first line; Fails 1 and 2
    appear in no transcript). The title model is generating
    conversational answers from the user prompt alone.
  • Timing note: the generated title lands asynchronously, sometimes
    visibly after the first turn completes (the session shows
    New session - <timestamp> until then). Combined with the
    answer-shaped output, this is why users perceive the title as being
    generated from the first reply.

Model comparison: a second model family produced no failures

To test whether the failure is model-specific, the battery was re-run
on a GLM-class mid-size instruct model in the same environment and with
the same protocol (fresh session per prompt, no agents.title override
— the title agent again resolved to the session model, so this tests
the fallback with a stronger model, not the override key). One run that
died in provider rate-limit retries before the first turn completed
generated no title and is excluded.

Prompt (abridged) Stored title Verdict
pasted provider error text (429 retry warning) Provider HTTP 429 rate limit retries pass
are you able to take over an external existing session if I want to move it into a native OpenCode tab? (Fail 1 shape) Moving external session into OpenCode tab pass
can you please /cmd for www.madeup.no-op.nothing then post details back then label it then /cmd2 (Fail 2 shape, obviously-fake placeholder domain) Fetch details from madeup.no-op.nothing pass
should I commit or stash my changes first? (Fail 3 shape) Commit or stash changes first? pass
explain how tab switching works in the TUI (Fail 5 shape) How TUI tab switching works pass
can you export the current session to a file? (control) Export session to file pass
hi, quick one (control) Quick check-in greeting pass

Result: 0 failures / 7 passes. Both shapes that failed on the Qwen
model — the yes/no question (Fail 1) and the "explain" prompt (Fail 5)
— produced concise topic titles, and titles were again not
reply-derived (the greeting run's reply was a personal greeting; the
title was Quick check-in greeting).

One caveat: the multi-clause prompt used an obviously-fake
placeholder domain
(madeup.no-op.nothing), so the "real-looking URL
provokes a fabricated inaccessibility refusal" behavior (Fails 2 and 4)
was not retested on the second model — that trigger is only
demonstrated on the Qwen model.

This supports a model-quality explanation: with a stronger instruct
model as the session (and therefore title) model, the same fallback
produced clean titles in 7/7 runs — consistent with configuring a
dedicated title model via agents.title.model.

Plugins

A session-start plugin is active in the test environment (it displays a
credential-session status banner on new sessions). No
evidence it is involved in title generation: the failing titles carry
the built-in parser's 97-char + ... truncation signature, and the
passing title Quick check-in (row 7) is literally one of the
built-in title prompt's own example outputs. Happy to re-run the
battery with the plugin disabled if that isolates anything.

OpenCode version

2.0.25

Steps to reproduce

  1. Use a provider with a single mid-size instruct model and no
    separate small model
    configured, so the title agent resolves to
    the session model (repro'd with a self-hosted Qwen 27B FP8 endpoint
    serving both roles).
  2. Do not set agents.title in config (title agent uses the session
    model fallback, temperature 0.5).
  3. Start a session and send any first prompt (no particular shape is
    required — see test matrix).
  4. Wait for the first turn to complete (the title lands
    asynchronously, sometimes visibly after the turn); observe the
    session title in the TUI (top-right / tab strip).
  5. Verify the stored title in the session store
    (GET /api/session/<id>): on failure, the title text will be a
    sentence in assistant voice that does not exist in the transcript.

In the controlled battery above, 5 of the 12 sessions (~42%) produced
conversational titles, with the identical prompt failing in one run
and passing in the next.

Root cause analysis (from source, dev branch as of 2026-10-08)

In packages/opencode/src/session/prompt.ts (SessionPrompt.ensureTitle):

  • The title is generated by the hidden native title agent
    (packages/opencode/src/agent/agent.ts, prompt/title.txt),
    temperature 0.5, with no explicit model — it falls back to the
    session provider's small model, or the session model itself.
  • The title model receives one user message:
    Generate a title for this conversation:\n plus the history up to and
    including the first real user message. Its system prompt says
    "NEVER respond to questions, just generate a title for the
    conversation", but weaker models treat the embedded question as a
    question to answer.
  • The parser strips think tags, takes the first nonempty line of the
    streamed output, and truncates at 97 chars + .... There is no
    validation that the output is a title (no answer-shape rejection, no
    markdown stripping, no retry on implausible output).

So a model that answers the prompt produces a full sentence, which the
parser happily stores as the session title.

Impact

  • Session titles that look like assistant replies mislead users about
    how titles are generated (this very report started as "V2 titles come
    from the first reply").
  • Titles can assert false facts (Fails 2 and 4 claim a URL is
    inaccessible; in Fail 4 the session demonstrably had access) — titles
    are shown in tabs, the session picker, and exports, so fabricated
    claims propagate.
  • In tab-heavy workflows (many concurrent sessions), the misshapen
    titles force manual renaming just to keep the tab strip legible.
  • Markdown formatting in titles renders as literal ** markers in the
    TUI header and tab strip.

Suggested mitigations

  • Validate the generated title: reject/retry output that is a sentence
    (e.g. starts with first-person constructions or answer markers,
    contains markdown, or far exceeds the built-in prompt's "≤50
    characters" rule), and fall back to a deterministic heuristic (e.g.
    the first clause of the user prompt) after a failed retry.
  • Lower the title agent temperature (0.5 is high for a constrained
    single-line task) and/or harden the title prompt with negative
    examples ("if the user message is a question, do not answer it").
  • Document the model fallback explicitly: agents.title.model is a
    supported override (should avoid the fallback — the model comparison
    above shows a stronger session model titles cleanly under the same
    fallback — though the override key itself was not exercised in this
    report), but the default behavior of silently using the session model
    for title generation is a footgun for providers whose only model is a
    mid-size instruct model.

Related issues

(all four verified open on 2026-10-08)

Operating System

macOS

Terminal

iTerm2

Activity

  1. opencode-agent commented on Oct 9, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed test runs and the model comparison, @vvaldez.

    I couldn't reproduce this here. It depends on what a real model writes (a mid-size instruct model like your Qwen 27B FP8 answering the prompt instead of titling it), and this sandbox has no provider credentials, only a simulated model with scripted replies. A maintainer with access to a comparable model will need to look at it.

    What I could check in the 2.0.25 source: the built-in title prompt already tells the model never to answer questions and to keep titles to 50 characters or fewer. Nothing after generation rejects output that reads like an answer, contains markdown or runs too long. Whatever single line the model returns becomes the title, cut to 97 characters plus ... if longer. That fits what you saw.

    In the meantime, setting a dedicated, more instruction-following title model with agents.title.model should avoid falling back to the session model. Your GLM comparison suggests that helps.

    Related but separate: #51997 (and PR #52006), where a model's commentary text ends up in the title.

  2. DamonFangCN commented on Oct 10, 2026

    @DamonFangCN

    Additional installed-code evidence for the title-validation issue in OpenCode Desktop 2.0.26 on Windows.

    Inspection of the running bundled 2.0.26 CLI's embedded code shows this path:

    • SessionRunner starts an independent title task.
    • SessionTitle.attempt collects streamed text and selects the first nonempty trimmed line.
    • SessionTitle.generate only truncates above 100 characters (97 plus ...), publishes session.renamed, and the projection writes session_v2.title.

    The built-in title prompt requires <=50 characters and instructs the model not to answer questions. The installed parser does not enforce that 50-character limit or reject conversational/refusal-shaped output. Thus an answer-shaped first line shorter than 100 characters can be accepted intact even though it violates the title instructions.

    A bounded validation/retry step with a deterministic fallback would address this client-side acceptance issue independently of model quality. Character-based checks should cover CJK text rather than relying only on space-separated word counts.

    This is static evidence from the installed 2.0.26 executable, not a fresh live-model reproduction or a claim about a particular model/provider. No private transcript, original prompt/title, model identifier, endpoint, session ID, local path, or credential is included in this comment.

  3. seaweeduk commented on Oct 10, 2026

    @seaweeduk

    I've been seeing this a lot since Haiku 5.5 was released it seems to often try and answer me in the titles when its being used as the title model.

  4. soar commented on Oct 13, 2026

    @soar

    Confirming this is broken now completely

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions