anomalyco / anomalyco/opencode

Session exits silently (exit 0, no error) when a turn returns finish "unknown": prompt.ts:1113 omits it from the loop-exit exclusion

Open
#43,622 6 comments 0 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Aug 20, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

packages/opencode/src/session/prompt.ts:1113 treats an assistant finish of "unknown" as a completed turn and breaks the agent loop. The session logs "exiting loop", disposes the instance and exits 0 with no error, discarding the turn. For a headless driver this is indistinguishable from the agent deciding it was done.

About 180 lines later the same file disagrees with itself: :1295 computes finished as finish && !["tool-calls", "unknown"].includes(finish), correctly excluding "unknown", while :1113 excludes only "tool-calls".

// :1113
if (lastAssistant?.finish &&
    !["tool-calls"].includes(lastAssistant.finish) &&
    !hasToolCalls && lastAssistant.parentID === lastUser.id) {
  log.info("exiting loop", ...); break
}

// :1295
const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)

Reproduction (v1.18.19, minimal, no real provider needed)

A local OpenAI-compatible server. The only variable between runs is whether any chunk carries a non-null finish_reason.

build finish_reason sent steps result
v1.18.19 stock none 1 reason: unknown, "exiting loop", exit 0, silent
v1.18.19 + "unknown" added at :1113 none 138+ never exits
v1.18.19 stock (control) "stop" 1 reason: stop, normal

Log from the first row:

message=stream providerID=fake modelID=fake-model
message=loop session.id=... step=1
message="exiting loop"
message="disposing instance"

37 ms, no error event, no stderr, exit 0.

Real-world impact

Six of sixteen headless implement sessions died this way in a single day driving opencode-go/deepseek-v4-flash, across five unrelated tasks at 46k to 230k context. Identical logs each time, with step_finish reporting all-zero usage and zero cost. An external driver reasonably reads exit 0 with no error as success, so the work is silently dropped.

Please do not ship the one-liner on its own

Adding "unknown" at :1113 removes the silent exit but produces an unbounded retry loop (138 steps, no cap observed). The shape that already works is at :1301, where a content-filter finish is surfaced as an error, with the comment "previously the session went idle silently". Same bug class, already fixed once for a different finish reason.

Note on history

An older checkout carries !["tool-calls", "unknown"] at the equivalent site. It is absent from v1.15.0, v1.16.0, v1.17.0, v1.18.0, v1.18.10, v1.18.18 and v1.18.19, and the surrounding condition was refactored somewhere in that window (!hasToolCalls added, the id comparison changed to parentID). It reads as a guard dropped during a refactor rather than a deliberate removal.

Environment

  • opencode 1.18.18 and 1.18.19, macOS arm64
  • Headless opencode run --format json, driven by an external loop
  • Reproduced against a local fake provider, so it is not provider-specific

Repro script and full session logs available on request.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.