Skip to content

Bug: V2 session titles include model commentary #51997

Closed

Description

@shanebishop1

Description

Some session titles start with planning text such as We need title only..., even though the model returned a valid title afterward.

Reproduced with openai/gpt-6-luna. The model sometimes returns two text messages which include its planning commentary.

commentary:   We need title only. Need capture investigation. Title concise.
final_answer: Read-only performance investigation

SessionTitle joins both messages and takes the first nonempty line, ignoring their phases. I would expect only the final title to be saved.

Plugins

N/A

OpenCode version

2.0.18

Steps to reproduce

  1. Use openai/gpt-6-luna as the title model with the none variant and built-in title prompt.
  2. Request a read-only performance investigation across several machines.
  3. When the title response includes commentary (live reproduction is intermittent), the saved title starts with it

A fixture with the two message phases above reproduces deterministically through the production parser and title service.

Screenshot and/or share link

Image

Operating System

macOS

Terminal

cmux

Activity

  1. github-actions commented on Sep 29, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of an existing issue. Please check:

    • #948: Generated titles sometimes include response text instead of being concise titles, causing non-title model output to appear in the saved session title.
  2. shanebishop1 commented on Sep 29, 2026

    @shanebishop1
    ContributorAuthor

    #52006 addresses this

  3. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the clear report. I reproduced this on 2.0.18, and it still happens on the latest dev (86ce19d).

    Steps: I pointed OpenCode at a local mock OpenAI Responses server that returns two messages for every request. The first has phase: "commentary" (We need title only. Need capture investigation. Title concise.) and the second has phase: "final_answer" (Read-only performance investigation). I used the mock as the session model and the title model, ran opencode run "Do a read-only performance investigation across several machines", and checked opencode session list. The saved title was:

    We need title only. Need capture investigation. Title concise.Read-only performance investigation

    The commentary and the final answer are joined with no separator, so the first-line selection doesn't separate them either. The expected title is just Read-only performance investigation. The script below reproduces it with the CLI.

    Related: your PR #52006 (excludes commentary-phase text), #57071 (strips inline reasoning blocks from titles) and #54185 (models replying conversationally instead of giving a title, which has a different cause).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces anomalyco/opencode#51997 with the CLI and a mock OpenAI Responses server.
    # Usage: ./repro.sh            -> uses `opencode` on PATH
    #        OC_DEV=/path/to/repo ./repro.sh -> runs the CLI from a source checkout
    set -euo pipefail
    WORK="$(mktemp -d /tmp/opencode/repro-51997.XXXX)"
    mkdir -p "$WORK/project" "$WORK/config" "$WORK/home"
    cat > "$WORK/mock.ts" <<'TS'
    // Mock OpenAI Responses server: every reply has a 'commentary' message then a 'final_answer' message.
    const COMMENTARY = "We need title only. Need capture investigation. Title concise."
    const FINAL = "Read-only performance investigation"
    const sse = (events: object[]) => events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("")
    const message = (id: string, phase: string, text: string, seq: { n: number }, index: number) => {
      const item = { id, type: "message", role: "assistant", status: "completed", phase, content: [{ type: "output_text", text, annotations: [] }] }
      return [
        { type: "response.output_item.added", output_index: index, sequence_number: seq.n++, item: { ...item, status: "in_progress", content: [] } },
        { type: "response.content_part.added", item_id: id, output_index: index, content_index: 0, sequence_number: seq.n++, part: { type: "output_text", text: "", annotations: [] } },
        { type: "response.output_text.delta", item_id: id, output_index: index, content_index: 0, sequence_number: seq.n++, delta: text },
        { type: "response.output_text.done", item_id: id, output_index: index, content_index: 0, sequence_number: seq.n++, text },
        { type: "response.output_item.done", output_index: index, sequence_number: seq.n++, item },
      ]
    }
    
    const requests: string[] = []
    const server = Bun.serve({
      port: 0,
      async fetch(req) {
        requests.push(`${req.method} ${new URL(req.url).pathname}`)
        await req.text()
        const seq = { n: 0 }
        const resp = { id: "resp_1", object: "response", status: "in_progress", model: "gpt-6-luna", output: [] }
        const body = sse([
          { type: "response.created", sequence_number: seq.n++, response: resp },
          ...message("msg_commentary", "commentary", COMMENTARY, seq, 0),
          ...message("msg_final", "final_answer", FINAL, seq, 1),
          {
            type: "response.completed",
            sequence_number: seq.n++,
            response: { ...resp, status: "completed", usage: { input_tokens: 10, output_tokens: 10, total_tokens: 20 } },
          },
        ])
        return new Response(body, { headers: { "content-type": "text/event-stream" } })
      },
    })
    console.log(`MOCK_PORT=${server.port}`)
    TS
    bun "$WORK/mock.ts" > "$WORK/mock.log" 2>&1 &
    MOCK=$!
    trap 'kill $MOCK 2>/dev/null || true' EXIT
    for _ in $(seq 50); do grep -q MOCK_PORT "$WORK/mock.log" && break; sleep 0.1; done
    PORT=$(sed -n 's/MOCK_PORT=//p' "$WORK/mock.log")
    cat > "$WORK/config/opencode.jsonc" <<JSON
    {
      "model": "mock/gpt-6-luna",
      "autoupdate": false,
      "providers": {
        "mock": {
          "name": "Mock OpenAI",
          "package": "@opencode-ai/ai/providers/openai/responses",
          "settings": { "apiKey": "mock-key", "baseURL": "http://127.0.0.1:$PORT/v1" },
          "models": {
            "gpt-6-luna": {
              "name": "GPT-6 Luna (mock)",
              "capabilities": { "tools": true, "input": ["text"], "output": ["text"] },
              "limit": { "context": 128000, "output": 16000 }
            }
          }
        }
      },
      "agents": { "title": { "model": { "providerID": "mock", "model": "gpt-6-luna" } } }
    }
    JSON
    export OPENCODE_CONFIG_DIR="$WORK/config" HOME="$WORK/home" XDG_DATA_HOME="$WORK/home/.local/share" XDG_CONFIG_HOME="$WORK/home/.config" XDG_STATE_HOME="$WORK/home/.local/state" XDG_CACHE_HOME="$WORK/home/.cache"
    if [ -n "${OC_DEV:-}" ]; then
      # the standalone server child process needs the opentui preload too; bunfig.toml in cwd provides it
      printf 'preload = ["%s"]\n' "$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" > "$WORK/project/bunfig.toml"
      oc() { bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts" "$@"; }
    else
      oc() { opencode "$@"; }
    fi
    cd "$WORK/project"
    echo "version: $(oc --version)"
    oc run --standalone "Do a read-only performance investigation across several machines" || true
    sleep 3
    echo "--- sessions ---"
    oc session list --standalone 2>&1 || oc session list 2>&1 || true
    echo "--- mock requests ---"; cat "$WORK/mock.log"

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions