Skip to content

Agent loop runs 500+ steps with zero user input — no guard against runaway auto-continue #49042

Closed as not planned

Description

@miguelvalenciav

Environment

  • opencode TUI, Linux, custom OpenAI-compatible provider (@ai-sdk/openai-compatible → koa-proxy → z.ai GLM-5.2).
  • Session storage: ~/.local/share/opencode/opencode.db (SQLite).

What happens

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

  1. Use a reasoning-capable model via OpenAI-compatible provider that occasionally returns empty content.
  2. Let a task complete; observe step=N climbing in the logs with no input.
  3. 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).

Activity

  1. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. domondi1 commented on Oct 6, 2026

    @domondi1

    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
    {
      "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:

    • 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.

  3. domondi1 commented on Oct 7, 2026

    @domondi1

    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.

    pip install -U inferrail          # 0.4.16
    inferrail serve --quickstart --app-mode --work-id-header x-opencode-session-id

    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.

  4. Nouron-labs commented on Oct 7, 2026

    @Nouron-labs

    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.

  5. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    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.

  6. opencodejr commented on Oct 29, 2026

    @opencodejr
    Contributor

    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!

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions