Skip to content

session: auto-continue after a stream interruption is delivered as an unmarked synthetic user message #56295

Description

@RateteDev

Description

When a response stream ends without finish_reason, OpenCode injects the synthetic user message "The previous response was interrupted. Continue from where you left off without repeating completed content." and continues the turn. That message reaches the model as plain user-role text with no mark that it was generated automatically, so the model cannot distinguish it from actual user input and can treat it as new user intent.

In my case the stream was cut right after the model had asked me to approve a plan. The model took the continuation as my approval and posted GitHub comments and pushed commits before I had replied with GO. Its reasoning states that the user had said "GO"; there is no GO in the actual user messages.

This auto-continue occurred 36 times in about six weeks across recent 2.0.x builds in my setup. Most are handled as ordinary continuations; the damage happens when the cut coincides with an approval wait.

Proposal: mark this synthetic user message so the model can tell it is system-generated and not user input. For example, add a [synthetic]-style prefix to the text, or deliver it through the existing system-update path (developer on Responses, <system-update> on Chat protocols). It must stay functional as a continuation instruction.

Related: #41358 (auto-continue after compaction), #54180 / #55076 (automatic resume after restart/kill), #41866 (a synthetic user instruction that overrode review-only scope). The triggers are different from this one.

Plugins

Local plugins are active, but this is core session-runner behavior and does not depend on a plugin.

OpenCode version

2.0.26

Steps to reproduce

  1. Have the model output a message asking the user to approve a change (waiting for GO).
  2. Before the turn completes, the response stream ends without finish_reason (a transient provider-side failure; in my case OpenCode Go / deepseek-v4.1-flash (OpenAI Chat compatible), recorded as provider.invalid-output: "OpenAI Chat stream ended without finish_reason").
  3. The harness injects the continuation message.
  4. The model interprets it as the user's approval and acts before the approval arrives.

Reproduction is probabilistic; the cut has to coincide with the approval wait.

Screenshot and/or share link

No response

Operating System

Ubuntu 26.04 (x86_64)

Terminal

N/A (Web UI)

Activity

  1. opencode-agent commented on Oct 17, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.0.26, and it still happens on the latest dev (512165c).

    Setup: the simulated OpenAI Chat model streams "Plan: push the fix and comment on the issue. Reply GO to approve." and then the connection drops before any finish_reason. OpenCode retries, and the next request to the model ends with:

    user:      Fix the bug, but wait for my GO before pushing.
    assistant: Plan: push the fix and comment on the issue. Reply GO to approve.
    user:      The previous response was interrupted. Continue from where you left off without repeating completed content.
    

    The continuation goes out as a plain user message with nothing marking it as automatic. Right after an approval request, it looks like the user replying. All synthetic messages are converted this way in packages/core/src/session/runner/to-llm-message.ts, while system messages already go through the <system-update> / developer path.

    One limit: I cut the stream with a dropped connection rather than your exact "stream ended without finish_reason" error. Both lead to the same continuation code. The simulated model also can't show a real model taking this as approval; the test only shows that nothing marks the message as automatic.

    The recording and screenshot below show the run, and the script below reproduces it.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#56295: after an OpenAI Chat stream ends without
    // finish_reason, OpenCode auto-continues by sending a plain user-role message,
    // indistinguishable from real user input.
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const requests: Array<unknown> = []
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* llm.serve((request: any, index) => {
            requests.push(request.body)
            if (index === 0)
              // Ask for approval, then cut the stream before any finish_reason arrives.
              return Stream.make(
                Llm.text("Plan: push the fix and comment on the issue. Reply GO to approve."),
                Llm.disconnect(),
              )
            return Stream.make(Llm.text("Continuing."))
          })
          yield* ui.submit("Fix the bug, but wait for my GO before pushing.")
          yield* ui.waitFor("Continuing.", { timeout: 30000 }).pipe(
            Effect.catchTag("UiWaitTimeoutError", (e: any) =>
              Effect.sync(() => console.log("TIMEOUT; requests:", requests.length, JSON.stringify(e.frame?.lines?.map((l: any) => l.text ?? JSON.stringify(l)).join("\n")).slice(0, 4000))),
            ),
          )
          yield* ui.screenshot("after-continue")
          const last: any = requests[requests.length - 1]
          const msgs = last?.messages ?? []
          console.log("REQUEST COUNT", requests.length)
          console.log("LAST REQUEST MESSAGES (tail):")
          console.log(JSON.stringify(msgs.slice(-3), null, 2))
        }),
    )

    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions