Repository navigation
Title agent produces conversational replies instead of session titles (V2) #54185
Description
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 titleCommit 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 theexample.complaceholder
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
- 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). - Do not set
agents.titlein config (title agent uses the session
model fallback, temperature 0.5). - Start a session and send any first prompt (no particular shape is
required — see test matrix). - 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). - 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
titleagent
(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:\nplus 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.modelis 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)
- Bug: V2 session titles include model commentary #51997 — V2 session titles include model commentary (same parser
weakness: first nonempty line of a multi-phase output) - Session title agent generates title from injected memory/system context rather than actual user message #23114 — title generated from injected memory/system context rather
than the user message - [FEATURE]: Make title-generation attachments opt-in to reduce privacy, token, and latency costs #50654 — make title-generation attachments opt-in
- [FEATURE]: Configurable max word count for generated session titles #43118 — configurable max word count for generated session titles
Operating System
macOS
Terminal
iTerm2
Activity
opencode-agent commentedon Oct 9, 2026
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.modelshould 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.
DamonFangCN commentedon Oct 10, 2026
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:
SessionRunnerstarts an independent title task.SessionTitle.attemptcollects streamed text and selects the first nonempty trimmed line.SessionTitle.generateonly truncates above 100 characters (97 plus...), publishessession.renamed, and the projection writessession_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.
seaweeduk commentedon Oct 10, 2026
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.
soar commentedon Oct 13, 2026
Confirming this is broken now completely