What happens
A thread backed by the OpenCode provider has its user-set title repeatedly overwritten with OpenCode's auto-generated session title. I create a thread with an explicit title (thread.create with title), never send titleSeed or regenerateTitle, and the title is replaced within seconds — and then again, and again. Measured 13 and 38 overwrites on two threads in a single session.
thread.meta.update (the "rename" path) does hold the title, but only until the next provider event, so re-asserting it is a race that can't be won.
Threads on claudeAgent, grok and cursor keep their titles correctly. Only OpenCode (and, more quietly, Codex) is affected.
Root cause
canReplaceThreadTitle (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:165) is not on this code path. Its only two call sites (:1052, :800) gate T3's own first-turn title generator. The provider-title mirror never consults it:
apps/server/src/provider/Layers/OpenCodeAdapter.ts:199-205 reads title from every OpenCode session.updated event.
OpenCodeAdapter.ts:785-825 emits it as thread.metadata.updated with payload.name, guarded only by if (title).
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:1707-1714 dispatches that straight into thread.meta.update — no guard, no dedup, no comparison against the thread's current title:
if (event.type === "thread.metadata.updated" && event.payload.name) {
yield* orchestrationEngine.dispatch({
type: "thread.meta.update",
commandId: yield* providerCommandId(event, "thread-meta-update"),
threadId: thread.id,
title: event.payload.name,
});
}
The repetition count comes from the OpenCode side: OpenCode's patch() publishes session.updated carrying the entire session record — including title — on every session mutation, including touch() on each prompt (packages/opencode/src/session/session.ts:735, :752). Their sync layer documents this as deliberate back-compat behaviour. So one generated title gets re-applied on every unrelated session change.
ClaudeAdapter, GrokAdapter and CursorAdapter never emit thread.metadata.updated, which is exactly why their titles survive. CodexAdapter.ts:712-732 has the same unguarded path but fires only on the dedicated, infrequent thread/name/updated notification.
Also relevant: T3 never gives OpenCode a title. OpenCodeAdapter.ts:1303-1312 calls client.session.create({ permission }) with no title, so OpenCode auto-names the session and T3 mirrors that name back onto a thread the user already named.
Suggested fix
Two options, not mutually exclusive:
-
Pass the thread title at session creation (smallest, no behaviour change elsewhere). OpenCode's CreateInput accepts title (packages/opencode/src/session/session.ts:260-270), and its titler backs off from any non-default title (packages/opencode/src/session/prompt.ts:199-200, isDefaultTitle). Sending the title at OpenCodeAdapter.ts:1303 means OpenCode never generates one and the mirror becomes a self-consistent no-op.
-
Guard the shared mirror at ProviderRuntimeIngestion.ts:1707 with canReplaceThreadTitle (currently module-private in ProviderCommandReactor.ts). ~5 lines, and it closes the same hole for Codex — though that is a deliberate semantic call, since a Codex thread would stop following Codex-side renames once T3 has a non-default title.
A titleLocked / titleSource field, or an expectedTitle compare-and-swap on thread.meta.update (mirroring the existing expectedBranch at apps/server/src/orchestration/decider.ts:639-644), would be the more thorough fix — there is currently no way for any client to express "this title is mine". Grepping titleLocked|userTitle|pinnedTitle|titleSource|titleOwner returns zero matches.
Steps to reproduce
- Create a thread on an OpenCode instance with an explicit non-default title (e.g.
My custom name).
- Send it a prompt.
- Watch the sidebar: the title is replaced by OpenCode's generated name, and re-replaced on each subsequent session mutation.
- Renaming the thread manually holds only until the next provider event.
Version
Reproduced on nightly v0.0.32-nightly.20260802.980. Code references above are from commit e60821f0e0d82a5d671ca3b94719c49d333921c8. OpenCode side read at anomalyco/opencode@1882c33; running opencode binary reports 1.18.11.
What happens
A thread backed by the OpenCode provider has its user-set title repeatedly overwritten with OpenCode's auto-generated session title. I create a thread with an explicit title (
thread.createwithtitle), never sendtitleSeedorregenerateTitle, and the title is replaced within seconds — and then again, and again. Measured 13 and 38 overwrites on two threads in a single session.thread.meta.update(the "rename" path) does hold the title, but only until the next provider event, so re-asserting it is a race that can't be won.Threads on
claudeAgent,grokandcursorkeep their titles correctly. Only OpenCode (and, more quietly, Codex) is affected.Root cause
canReplaceThreadTitle(apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:165) is not on this code path. Its only two call sites (:1052,:800) gate T3's own first-turn title generator. The provider-title mirror never consults it:apps/server/src/provider/Layers/OpenCodeAdapter.ts:199-205readstitlefrom every OpenCodesession.updatedevent.OpenCodeAdapter.ts:785-825emits it asthread.metadata.updatedwithpayload.name, guarded only byif (title).apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:1707-1714dispatches that straight intothread.meta.update— no guard, no dedup, no comparison against the thread's current title:The repetition count comes from the OpenCode side: OpenCode's
patch()publishessession.updatedcarrying the entire session record — includingtitle— on every session mutation, includingtouch()on each prompt (packages/opencode/src/session/session.ts:735,:752). Their sync layer documents this as deliberate back-compat behaviour. So one generated title gets re-applied on every unrelated session change.ClaudeAdapter,GrokAdapterandCursorAdapternever emitthread.metadata.updated, which is exactly why their titles survive.CodexAdapter.ts:712-732has the same unguarded path but fires only on the dedicated, infrequentthread/name/updatednotification.Also relevant: T3 never gives OpenCode a title.
OpenCodeAdapter.ts:1303-1312callsclient.session.create({ permission })with notitle, so OpenCode auto-names the session and T3 mirrors that name back onto a thread the user already named.Suggested fix
Two options, not mutually exclusive:
Pass the thread title at session creation (smallest, no behaviour change elsewhere). OpenCode's
CreateInputacceptstitle(packages/opencode/src/session/session.ts:260-270), and its titler backs off from any non-default title (packages/opencode/src/session/prompt.ts:199-200,isDefaultTitle). Sending the title atOpenCodeAdapter.ts:1303means OpenCode never generates one and the mirror becomes a self-consistent no-op.Guard the shared mirror at
ProviderRuntimeIngestion.ts:1707withcanReplaceThreadTitle(currently module-private inProviderCommandReactor.ts). ~5 lines, and it closes the same hole for Codex — though that is a deliberate semantic call, since a Codex thread would stop following Codex-side renames once T3 has a non-default title.A
titleLocked/titleSourcefield, or anexpectedTitlecompare-and-swap onthread.meta.update(mirroring the existingexpectedBranchatapps/server/src/orchestration/decider.ts:639-644), would be the more thorough fix — there is currently no way for any client to express "this title is mine". GreppingtitleLocked|userTitle|pinnedTitle|titleSource|titleOwnerreturns zero matches.Steps to reproduce
My custom name).Version
Reproduced on nightly
v0.0.32-nightly.20260802.980. Code references above are from commite60821f0e0d82a5d671ca3b94719c49d333921c8. OpenCode side read atanomalyco/opencode@1882c33; runningopencodebinary reports1.18.11.