Skip to content

doom_loop never fires when repeated calls span steps: check only reads the current assistant message #51965

Closed as not planned

Description

@calebtt

Description

The doom_loop check in packages/opencode/src/session/processor.ts (around line 353 on dev) only reads the parts of the current assistant message:

const parts = yield* MessageV2.parts(ctx.assistantMessage.id)
const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD)

Each step is its own assistant message. When the model makes one tool call per step, that message holds [step-start, tool] at the time of the check, so recentParts.length is 2 and the check returns early. The guard cannot fire however many times a call repeats across steps. It only catches three identical calls inside a single response.

This was reported in #25254, which the stale bot closed with a request to open a new issue if it was still relevant. It is still present in v1.18.33 and on dev. PR #32089 changes the check to count matching tool parts across the session and has been open since June. #45442 looks like the same cause, seen from a subagent.

What we saw. v1.18.33, local model through @ai-sdk/openai-compatible. In an 83-step session, every assistant message had exactly three parts: step-start, one tool, step-finish. From step 13 on, the model alternated between two bash calls about 70 times over roughly 10 minutes, until it was cancelled. Two user messages sent during the loop, one pointing it out and one redirecting the task, did not change the next call. The log has no doom_loop evaluation for the session. An earlier session on v1.18.32 alternated between two commands for 183 calls in the same way.

The alternation is also the pattern in #47759, but the scope problem hides it first: even a single command repeated once per step is never seen.

Plugins

None

OpenCode version

1.18.33 (also seen on 1.18.32)

Steps to reproduce

  1. Leave doom_loop at its default (ask).
  2. Use a model that makes one tool call per response, e.g. a local model through @ai-sdk/openai-compatible.
  3. Get it to issue the same bash call with identical input in three or more consecutive steps.
  4. No doom_loop prompt appears. In the session DB, each step's assistant message has parts [step-start, tool, step-finish].

Operating System

Ubuntu 24.04.5 LTS

Activity

  1. github-actions commented on Sep 29, 2026

    @github-actions
    Contributor

    This issue may overlap with existing reports of doom_loop detection gaps. Please check:

  2. calebtt commented on Sep 29, 2026

    @calebtt
    Author

    Thanks. I checked all three before filing, and they're linked in the description.

    This issue covers only the scope of the check, and that gap blocks the other two. Once the check spans the calls since the last user message, it catches repeats like #45442's, and period detection for #47759 can build on it.

  3. gauntface commented on Oct 7, 2026

    @gauntface

    Just encountered this as well with same root cause. I believe the MessageV2.filterCompactedEffect(ctx.sessionID) change from #32089 would fix the immediate issue.

  4. lvjing2 commented on Oct 17, 2026

    @lvjing2

    I checked V2 dev 8c173a8f3805d8e749d44311054b07e37a1b2ce1. I couldn't find a cross-step repeat check in the runner; doom_loop still appears in the V1 config compatibility code. The runner has an optional steps limit, but that doesn't distinguish repetition from useful work.

    I ran a scripted reproduction on 22e771f5893e255ea78fc10557ed3fb98d3a1072; the relevant runner files are unchanged in the newer commit. Eight identical failed tool calls across eight steps all execute, followed by a ninth model request to stop. There are no provider retry events. With steps=5, only four tool bodies execute. This proves the harness behavior, not how often real models get stuck.

    Would a V2 follow-up here make sense, starting with consecutive identical calls across steps? I'd keep the history bounded and cover user input, intervening work and successful polling so legitimate retries aren't blocked. Changed-input failure feedback and periodic cycles could stay out of that first change.

    Minimal reproduction in the existing runner test file

    Add this scenario inside the existing SessionRunnerLLM describe block in packages/core/test/session-runner.test.ts:

    scenario("tool failure feedback repro: eight failed steps", function* (s) {
        const registry = yield* Tool.Service
        const calls: string[] = []
        yield* transformTools(registry, {
          lookup: {
            name: "lookup",
            description: "Look up a missing fixture resource",
            input: Schema.Struct({ resource: Schema.String }),
            output: Schema.Struct({}),
            execute: ({ resource }) => {
              calls.push(resource)
              return new Tool.Error({ message: "Resource not found" })
            },
          },
        }, { codemode: false })
        yield* s.admit("Look up the missing resource")
        yield* s.llm.push(
          ...Array.from({ length: 8 }, (_, index) =>
            TestLLM.tool(`lookup-${index}`, "lookup", { resource: "missing" }),
          ),
          TestLLM.stop(),
        )
        yield* s.resume
        expect(calls).toEqual(Array(8).fill("missing"))
        expect(s.requests).toHaveLength(9)
        const events = yield* recordedEventTypes(sessionID)
        expect(events.filter((type) => type === "session.tool.failed.2")).toHaveLength(8)
        expect(events).not.toContain("session.retry.scheduled.1")
      })

    From packages/core, using Bun 1.4.2:

    bun run script/test.ts test/session-runner.test.ts --test-name-pattern 'tool failure feedback repro'

    The minimal reproduction passes. The broader local baseline has six cases, all passing across ten repetitions; the full runner file and Core typecheck also pass. No runtime behavior has been changed.

  5. opencode-agent commented on Oct 19, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. This is against the V1 session processor, and the doom_loop check was removed in V2. Since V1 won't get a fix for this, we're closing it. Please update to the latest OpenCode release, and if you still see repeated tool-call loops there, open a new issue with your V2 version and repro steps.

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

Metadata

Metadata

Assignees

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