Skip to content

V2 TUI silently succeeds on empty length turns until manual compaction #52015

Description

@hgto

Description

In OpenCode v2.0.18's TUI, a long session stopped answering despite accepting prompts. Four consecutive assistant turns were stored with finish: "length", tokens.output: 0, and empty assistant content. Each was followed by an idle outcome of succeeded; the TUI showed no error. Manually running compaction made the session work again.

The model used an OpenAI-compatible provider and was configured for 1,050,000 context tokens and 128,000 output tokens. Input including cache rose from 921,222 to 921,238 tokens while reasoning fell from 344 to 328; input plus reasoning stayed at 921,566 for all four turns. No automatic compaction was recorded. This is consistent with context pressure, though the source of the effective cap is unconfirmed.

Expected: automatic recovery when safe, or a visible truncation error. An empty turn should not be marked successful. Related reports: #44609, #50949, and #37372; this report adds a v2.0.18 TUI reproduction and manual-compaction recovery.

Plugins

No plugin tool call occurred in the failing turns.

OpenCode version

v2.0.18

Steps to reproduce

  1. Use a reasoning model with a long, tool-heavy TUI session until input including cache is about 921,000 tokens under the limits above.
  2. Submit a normal request, then short follow-ups. The model runs, but no answer or error appears.
  3. Manually compact the session. Responses work again.

Screenshot and/or share link

Three unanswered prompts followed by manual compaction

Operating System

macOS 26.7 (arm64)

Terminal

tmux

Activity

  1. github-actions commented on Sep 29, 2026

    @github-actions
    Contributor

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

  2. Veld101 commented on Sep 29, 2026

    @Veld101

    This is #50949 (already flagged as the duplicate). A fix is open in #51741.

    The change: a normalized length finish with no streamed output now fails the Step with a visible error instead of settling as a success, so an empty turn stops looking like a healthy one. Partial output is unaffected - a length finish that streamed any content still settles normally with its visible text - and a length finish with tool calls still continues.

    Your detail that manual compaction recovers the session is consistent with the same code path: nothing marks the empty turn as failed, so there is no reason to surface an error or recover automatically. #51741 makes the failure visible; it does not add automatic recovery, which is the separate ask tracked in #44609.

  3. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed report. I reproduced this with a simulated model whose responses always finish with reason length and contain no text, reasoning or tool calls.

    • v2.0.18: each prompt produces one assistant message with finish: "length", empty content and 0 output tokens. The session goes idle, and the TUI shows no answer and no error. No automatic compaction is attempted.
    • Latest dev (86ce19d96): the output-limit continuation added in feat(core): continue responses after output token limits #53876 runs, so each prompt now produces three empty length turns. The TUI still shows no error, and compaction still isn't attempted.

    The recording below shows the v2.0.18 run, and the screenshot shows the empty turns on dev. The script below reproduces it.

    Related work: open PRs #56673 and #51741 both handle turns that finish with no output, and #44609 proposed compacting and retrying in this case. Until then, a manual compaction like the one you used is the workaround.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Reproduces #52015: a provider step that finishes with finish_reason "length"
    // and no content is settled as a successful, silent turn.
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
      },
      ({ ui, llm, opencode }) =>
        Effect.gen(function* () {
          let calls = 0
          // Every model request: empty stream, finish reason "length", no text/reasoning/tool calls.
          yield* llm.serve(() => {
            calls++
            return Stream.make(Llm.finish("length"))
          })
          yield* ui.screenshot("01-home")
          yield* ui.submit("Summarize the project status")
          yield* Effect.sleep("6 seconds")
          yield* ui.screenshot("02-after-first-prompt")
          yield* ui.submit("hello?")
          yield* Effect.sleep("6 seconds")
          yield* ui.screenshot("03-after-second-prompt")
          yield* Effect.log(`model requests served: ${calls}`)
          // Dump the stored assistant messages: finish, tokens and any error.
          const sessions: any = yield* opencode.session.list()
          for (const session of (sessions.items ?? sessions.data ?? sessions) as any[]) {
            const messages: any = yield* opencode.message.list({ sessionID: session.id } as any)
            for (const m of (messages.items ?? messages.data ?? messages) as any[])
              if (m.type === "assistant")
                yield* Effect.log(JSON.stringify({ finish: m.finish, tokens: m.tokens, error: m.error, content: m.content }))
          }
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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