#18500 · @luyanhexay · opened Mar 21, 2026 at 8:24 AM UTC · last updated Mar 21, 2026 at 8:40 AM UTC

fix(session): stop prompt loop after model completion

appfix
76
+1021 files

Score breakdown

Impact

8.0

Clarity

8.0

Urgency

7.0

Ease Of Review

8.0

Guidelines

9.0

Readiness

9.0

Size

10.0

Trust

5.0

Traction

6.0

Summary

This PR fixes a bug where the session prompt loop continued after model completion, causing duplicate assistant outputs or prefill errors. It ensures explicit loop termination for terminal states. This addresses issues particularly affecting Anthropic models.

Open in GitHub

Description

Issue for this PR

Closes #17982 Refs #18096 Refs #11153

What this PR changes

  • stops the session prompt loop when the model has already completed and there is no tool activity
  • treats finish=unknown as terminal only for non-tool responses to prevent extra assistant-message iterations
  • preserves existing json-schema and tool-call handling paths

Why this change is needed

A completed model response could still continue into another prompt-loop iteration, creating duplicate assistant outputs in one user turn. This patch makes loop termination explicit for that terminal state.

Type of change

  • [x] Bug fix
  • [ ] New feature
  • [ ] Breaking change
  • [ ] Documentation update

How this was tested

  • bun test ./packages/opencode/test/session/session.test.ts (3 passed, 0 failed)
  • repeated local reproduction with anthropic/claude-sonnet-4-6 no longer produced multi-assistant emissions after a single ping

Checklist

  • [x] I linked an existing issue
  • [x] I tested locally
  • [x] This PR stays focused on one bug fix area

Linked Issues

#17982 Bug: OpenCode prompt loop continues after `finish=stop`, triggering prefill error on claude-opus-4-6

View issue

Comments

No comments.

Changed Files

packages/opencode/src/session/prompt.ts

+102
@@ -696,8 +696,14 @@ export namespace SessionPrompt {
break
}
// Check if model finished (finish reason is not "tool-calls" or "unknown")
const modelFinished = processor.message.finish && !["tool-calls", "unknown"].includes(processor.message.finish)
const parts = await MessageV2.parts(processor.message.id)
const hasToolActivity = parts.some((part) => part.type === "tool")
const modelFinished =
!!processor.message.finish &&
(![
"tool-calls",
"unknown",
].includes(processor.message.finish) || (processor.message.finish === "unknown" && !hasToolActivity))
if (modelFinished && !processor.message.error) {
if (format.type === "json_schema") {
@@ -709,6 +715,8 @@ export namespace SessionPrompt {
await Session.updateMessage(processor.message)
break
}
break
}
if (result === "stop") break