Repository navigation
Description
Summary
When a local server cuts a stuck reply with its repetition/loop guard, it reports the reason honestly in finish_details: {"type": "repetition_loop"} (alongside finish_reason: "stop"). opencode reads only finish_reason and does not surface finish_details anywhere (not in the UI, not in the persisted message data — [inferred] dropped at parse time). The turn is then persisted as if it ended normally.
Consequence: everyone downstream is blind to what happened.
Environment
- opencode CLI (latest, macOS)
- Model served by
mlx-serve26.10.1 (OpenAI-compatible chat completions). Its loop guard cuts a stuck generation and returnsfinish_reason: "stop"+finish_details: {"type": "repetition_loop"}— this is documented in the engine's docs and log line ([loop-stop] ... details=repetition_loop). - The same field exists in other ecosystems under different spellings (e.g. llama.cpp-style servers report loop cuts distinctly too), so this is not mlx-serve-specific.
Reproduction (observed on our setup)
- Agent session, greedy decoding (temperature 0), long agentic tool chain.
- The model's thinking block falls into a degenerate cycle (a ~8-token Chinese phrase repeated 10+ times). The server's loop guard cuts it at ~4900 generated tokens and returns
finish_details: {"type": "repetition_loop"}. - In opencode's UI: the turn just ends after a wall of repeated text — no notice, no warning, looks like a normal (if weird) reply.
- In opencode's session database (
message.data), the turn is stored asfinish: "stop"— thefinish_detailsfield is nowhere. A follow-up turn by the model reading its own history therefore "sees" a normally-completed turn, and (as happened to us) the model confabulates that it recovered on its own.
Expected
opencode should surface loop-guard cuts instead of swallowing them:
- At minimum, persist
finish_details(or a normalized flag) on the assistant message, and show the user a notice like "reply stopped: repetition loop detected by server" — distinct from "output truncated" and from a clean stop. - Optionally, auto-retry the turn once (a loop cut is a transient sampling failure, not a content decision), similar to existing truncation handling.
Actual
finish_details never reaches the UI, the database, or what the model sees on the next turn — the turn is indistinguishable from a normal completion everywhere downstream.
Why it matters
The three parties involved each hold one piece of the truth and none of them get it:
- the server knows (it cut the turn and said why);
- the user sees a mysterious wall of repeated text with no explanation;
- the model, on the next turn, reads a history entry claiming a clean
stop— and will confabulate a narrative about what happened (ours literally reported "I recovered from the loop by sampling noise", which was false: the turn was beheaded by the token limit / loop guard).
This is a small parsing gap with outsized diagnostic value for local-model agent debugging.
Evidence
- server log:
[loop-stop] degenerate tail loop cut after 4900 generated tokens (finish_reason=stop details=repetition_loop tier=long_cycle trim_start=3925) - opencode db (
messagetable) for the same turn:finish = "stop", no trace of the loop detail. - engine doc: "a reply cut short because the model went in circles reports
finish_details: {"type": "repetition_loop"}besidefinish_reason."
Activity
github-actions commentedon Oct 6, 2026
This issue doesn't fully meet our contributing guidelines.
What needs to be fixed:
Please edit this issue to address the above within 72 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
finish=stop, which OpenCode records as a normal completion.If you believe this was flagged incorrectly, please let a maintainer know.
github-actions commentedon Oct 9, 2026
This issue has been automatically closed because it was not updated to meet our contributing guidelines within the 72-hour window.
Feel free to open a new issue that follows our issue templates.