You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Agent loop runs 500+ steps with zero user input — no guard against runaway auto-continue #49042
The agent loop auto-continues indefinitely after the model produces its final text response. Two sessions on 2026-09-14:
Session
Loop steps
user msgs (tail)
assistant msgs
Output tokens
ses_f5ea9de7bffeoBkzj24EyMMa3g ("Investigar conversación de bot en Chatwoot")
525 (66 min)
0 in the final 18 min
228 in the tail
53,572
ses_f5eab63c5ffeeuWv40uF3CZ9Zo
310
5 total (real human inputs)
310
34,599
Log signature: message=loop session.id=… step=N incrementing every ~5 s with no interleaved user activity. All messages share one parentID (the last real user message). The model's outputs in the runaway tail are minimal closings ("(fin)", "🤘", "·") — i.e. the model DID produce text-only terminal responses, but the loop kept stepping instead of ending the turn.
A contributing trigger observed with reasoning models: GLM-5.2 sometimes returns reasoning_content with empty content; opencode logs a placeholder _(reasoning-only response — model emitted no final text)_ and re-prompts. But the runaway also occurs after normal text responses, so the missing piece is a loop guard, not just empty-output handling.
Expected
After an assistant turn with no tool calls and no user input, the loop should end. At minimum: a guard that halts after N consecutive steps without human input or tool activity, surfacing "possible runaway loop — press Enter to continue".
Why doom_loop doesn't catch it
doom_loop permission triggers only on the same tool call repeating 3× with identical input. Text-only runaway has no tool calls at all — no knob covers it.
Repro
Use a reasoning-capable model via OpenAI-compatible provider that occasionally returns empty content.
Let a task complete; observe step=N climbing in the logs with no input.
Loop sustains itself for hours until manually interrupted (Esc).
Workaround
Esc / close session. A pending question tool call also parks the loop (it blocks awaiting real input).
The loop guard belongs in opencode, but until there is one, a hard spend ceiling outside the agent stops a 500-step tail from running for an hour. Since you're already on a custom @ai-sdk/openai-compatible provider, it's config only: put a local gateway in front of the provider that refuses calls with HTTP 402 before they reach it once a daily budget is used. Disclosure: I maintain the gateway (Inferrail, open source).
pip install inferrail
# the gateway holds the real key and forwards to your provider (OpenAI here; any OpenAI-compatible# endpoint works with a providers: entry and a price for the model in inferrail.yaml)export OPENAI_API_KEY=sk-...
inferrail serve --quickstart --app-mode # http://127.0.0.1:8000
inferrail budget set --scope project --scope-value opencode --window daily --mode block --limit-usd 5
What I checked with opencode 1.18.34 and inferrail 0.4.15, against a stand-in OpenAI endpoint with fixed token counts:
Normal runs: every call (including the title call) is recorded under opencode, with tokens and cost per call. inferrail report --by project shows the day's total.
Once the day's budget can't cover the next call: opencode prints Error: budget 'project:opencode:daily' ... would be exceeded and exits. 2 requests, both refused before the upstream, about 8 s, no retry loop.
Caveats: the cap is per day for everything sent with that header, not per session, because opencode's provider headers are static. Each call reserves its worst case up front from max_tokens (about $0.02 for gpt-4o-mini at opencode's default), so a budget smaller than that refuses immediately. For GLM via a z.ai or proxy endpoint you'd add that endpoint and the model's price in inferrail.yaml instead of --quickstart.
opencode already sends x-opencode-session-id on every request, so a per-session cap is possible without a wrapper too: that needs an option on the gateway that isn't released yet. I'll follow up here when it is.
Follow-up, as promised: inferrail 0.4.16 can cap each opencode session instead of the whole day, because opencode already sends x-opencode-session-id on every request.
and in the provider's options.headers, only a static amount: {"X-Inferrail-Budget-Usd": "3"}. Every session gets its own $3 ceiling, a runaway tail like the 525-step one above gets 402 before the provider once its session's budget is used, and inferrail report --by work_id lists cost per ses_... id. Checked with opencode 1.18.34 against a stand-in upstream. The daily project cap from my earlier comment still works alongside it.
Thanks for writing this up. Something that helped me think about this: the cap has to live outside the prompt, and whatever goes past the cap should be approved by a person. If the agent makes a mistake or loops, the damage is bounded by the cap.
A question for people living this: how do you put a limit on what your agent spends today, and what happened the last time it went over? I'm building something along these lines (early-stage idea, test mode, no real money) and real cases help me, here or in private.
Thanks for the detailed report and the session numbers!
You didn't mention a version, but the details (the loop ... step=N log line, and every assistant message sharing the last user message as parentID) match how the OpenCode v1 prompt loop works (opencode-ai, versions 0.x/1.x). In v1 the loop only ends a turn on certain finish reasons. If the provider reports a finish reason v1 doesn't recognise, it's recorded as unknown and the loop keeps prompting the model after every text reply. That can easily happen behind an OpenAI-compatible proxy like your koa-proxy, and it would explain a runaway made of short closing messages. Related reports:
v1 now only gets fixes for critical bugs (data loss, security, crashes or breakage with no workaround). Because Esc stops the loop, this one is unlikely to be fixed in v1.
We'd encourage you to try v2 (@opencode/cli, the opencode command). In v2 a turn only continues when a tool call actually ran, and an unknown finish reason is treated as an incomplete response instead of a reason to keep going. So a text-only runaway like this shouldn't happen there.
If you're actually on 2.x (opencode --version), or you see this on v2 too, please reply with the version and we'll look into it again. Please also reply if you think this counts as critical.
Closing this as not planned. The details point to OpenCode v1 (opencode-ai, 1.x), where the prompt loop keeps going when a provider's finish reason is recorded as unknown (see #46415 and #43939). v1 only gets critical fixes, and Esc stops the loop, so this is unlikely to change there.
In v2 (@opencode/cli), a turn only continues after a tool call runs, so a text-only runaway like this shouldn't happen. If you see it on 2.x, please comment with the output of opencode --version and the provider setup, and we'll reopen it. Thanks for the detailed report!
Activity
github-actions commentedon Sep 14, 2026
This issue might be a duplicate of existing issues. Please check:
domondi1 commentedon Oct 6, 2026
The loop guard belongs in opencode, but until there is one, a hard spend ceiling outside the agent stops a 500-step tail from running for an hour. Since you're already on a custom
@ai-sdk/openai-compatibleprovider, it's config only: put a local gateway in front of the provider that refuses calls with HTTP 402 before they reach it once a daily budget is used. Disclosure: I maintain the gateway (Inferrail, open source).{ "provider": { "capped": { "npm": "@ai-sdk/openai-compatible", "options": { "baseURL": "http://127.0.0.1:8000/v1", "apiKey": "unused", "headers": { "X-Inferrail-Attribute-Project": "opencode" } }, "models": { "gpt-4o-mini": { "name": "gpt-4o-mini" } } } }, "model": "capped/gpt-4o-mini" }What I checked with opencode 1.18.34 and inferrail 0.4.15, against a stand-in OpenAI endpoint with fixed token counts:
opencode, with tokens and cost per call.inferrail report --by projectshows the day's total.Error: budget 'project:opencode:daily' ... would be exceededand exits. 2 requests, both refused before the upstream, about 8 s, no retry loop.Caveats: the cap is per day for everything sent with that header, not per session, because opencode's provider headers are static. Each call reserves its worst case up front from
max_tokens(about $0.02 for gpt-4o-mini at opencode's default), so a budget smaller than that refuses immediately. For GLM via a z.ai or proxy endpoint you'd add that endpoint and the model's price ininferrail.yamlinstead of--quickstart.opencode already sends
x-opencode-session-idon every request, so a per-session cap is possible without a wrapper too: that needs an option on the gateway that isn't released yet. I'll follow up here when it is.domondi1 commentedon Oct 7, 2026
Follow-up, as promised: inferrail 0.4.16 can cap each opencode session instead of the whole day, because opencode already sends
x-opencode-session-idon every request.pip install -U inferrail # 0.4.16 inferrail serve --quickstart --app-mode --work-id-header x-opencode-session-idand in the provider's
options.headers, only a static amount:{"X-Inferrail-Budget-Usd": "3"}. Every session gets its own $3 ceiling, a runaway tail like the 525-step one above gets 402 before the provider once its session's budget is used, andinferrail report --by work_idlists cost perses_...id. Checked with opencode 1.18.34 against a stand-in upstream. The daily project cap from my earlier comment still works alongside it.Nouron-labs commentedon Oct 7, 2026
Thanks for writing this up. Something that helped me think about this: the cap has to live outside the prompt, and whatever goes past the cap should be approved by a person. If the agent makes a mistake or loops, the damage is bounded by the cap.
A question for people living this: how do you put a limit on what your agent spends today, and what happened the last time it went over? I'm building something along these lines (early-stage idea, test mode, no real money) and real cases help me, here or in private.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed report and the session numbers!
You didn't mention a version, but the details (the
loop ... step=Nlog line, and every assistant message sharing the last user message asparentID) match how the OpenCode v1 prompt loop works (opencode-ai, versions 0.x/1.x). In v1 the loop only ends a turn on certain finish reasons. If the provider reports a finish reason v1 doesn't recognise, it's recorded asunknownand the loop keeps prompting the model after every text reply. That can easily happen behind an OpenAI-compatible proxy like your koa-proxy, and it would explain a runaway made of short closing messages. Related reports:finish_reason: unknownkeeps the v1 loop running after a complete replyfinish=unknownv1 now only gets fixes for critical bugs (data loss, security, crashes or breakage with no workaround). Because Esc stops the loop, this one is unlikely to be fixed in v1.
We'd encourage you to try v2 (
@opencode/cli, theopencodecommand). In v2 a turn only continues when a tool call actually ran, and anunknownfinish reason is treated as an incomplete response instead of a reason to keep going. So a text-only runaway like this shouldn't happen there.If you're actually on 2.x (
opencode --version), or you see this on v2 too, please reply with the version and we'll look into it again. Please also reply if you think this counts as critical.opencodejr commentedon Oct 29, 2026
Closing this as not planned. The details point to OpenCode v1 (
opencode-ai, 1.x), where the prompt loop keeps going when a provider's finish reason is recorded asunknown(see #46415 and #43939). v1 only gets critical fixes, and Esc stops the loop, so this is unlikely to change there.In v2 (
@opencode/cli), a turn only continues after a tool call runs, so a text-only runaway like this shouldn't happen. If you see it on 2.x, please comment with the output ofopencode --versionand the provider setup, and we'll reopen it. Thanks for the detailed report!