Skip to content

[Bug]: OpenCode provider overwrites user-set thread titles repeatedly (provider title mirror bypasses canReplaceThreadTitle) #5245

Description

@JorgeMenaDev

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:

  1. apps/server/src/provider/Layers/OpenCodeAdapter.ts:199-205 reads title from every OpenCode session.updated event.
  2. OpenCodeAdapter.ts:785-825 emits it as thread.metadata.updated with payload.name, guarded only by if (title).
  3. apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:1707-1714 dispatches that straight into thread.meta.updateno 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:

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

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

  1. Create a thread on an OpenCode instance with an explicit non-default title (e.g. My custom name).
  2. Send it a prompt.
  3. Watch the sidebar: the title is replaced by OpenCode's generated name, and re-replaced on each subsequent session mutation.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    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