anomalyco / anomalyco/opencode

Unimplemented `commentary` channel in gpt.txt: progress updates end the turn on chat-completions models

Open
#47,168 5 comments 0 reactions 1 assignee View on GitHub

@neriousy is already working on this.

Since Sep 3, 2026.

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

Description

Description

opencode's GPT system prompt tells the model to send short progress updates on a commentary channel. Nothing in opencode implements that channel, and on chat-completions providers each such update ends the turn.

packages/opencode/src/session/prompt/gpt.txt:81-97 describes a commentary channel for progress updates and a final channel for the answer; :95 says "Before substantial work, send a short update describing your first step." At dev@d2efd81fb the word appears in two prompt text files and zero .ts/.tsx/.js files.

The prompt is picked by model.api.id.includes("gpt") (packages/opencode/src/session/system.ts:34, PROMPT_GPT at :38), so it also goes to plain chat-completions models. That path has no channel field: packages/llm/src/protocols/openai-compatible-chat.ts:17-21 reuses OpenAIChat.protocol against /chat/completions, and the delta schema at openai-chat.ts:145-154 parses only content, reasoning_content, tool_calls and finish_reason. A commentary update arrives as ordinary assistant text with finish_reason: "stop" and no tool parts.

That matches the loop-exit condition in packages/opencode/src/session/prompt.ts:1113: a finished assistant message with no tool parts is treated as completion. So a progress update requested by gpt.txt:95 can terminate the turn before tools run.

// packages/opencode/src/session/prompt.ts:1106-1117
const hasToolCalls = lastAssistantMsg?.parts.some(
  (part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),
) ?? false

if (
  lastAssistant?.finish &&
  !["tool-calls", "unknown"].includes(lastAssistant.finish) &&
  !hasToolCalls &&
  lastAssistant.parentID === lastUser.id
) {
  // ...
  yield* Effect.logInfo("exiting loop", { "session.id": sessionID })   // :1128
  break
}

Over one long chat-completions session: 1196 turns finished tool-calls, all carrying tool parts; 115 finished stop, all text-only; the log holds exactly 115 exiting loop lines for that session. Every text-only stop turn I inspected there had a matching exiting loop line.

This is not the finish: "unknown" family (#43622, #41469, #37852; #44146 is a generic premature-stop report the duplicate bot grouped there). Those turns have no usable finish reason and zero tokens, and "unknown" is already excluded at :1113. Here stop is honest and the message has real content — the guard does what it says, and the problem is upstream of it, in the prompt.

Fix: only emit the gpt.txt:81-97 block on a transport that implements channels — system.ts:27-48 dispatches on model id alone. Short-term mitigation: remove or gate the commentary instruction until a transport path supports it.

Search output, and the Responses side
$ rg -c -i 'commentary' . -g '!**/node_modules/**' -g '!**/dist/**' -g '!.git/**'
./packages/opencode/src/session/prompt/gpt.txt:5
./packages/opencode/src/agent/generate.txt:4

$ rg -n -i 'commentary' packages/ -g '*.ts' -g '*.tsx' -g '*.js' -g '!**/node_modules/**' -g '!**/dist/**'
(no output)

generate.txt's hits are <commentary> XML tags in few-shot examples for agent generation, not a response channel.

The Responses API does carry a phase field on message items, and recorded fixtures under packages/llm/test/fixtures/recordings/openai-responses/ contain "phase":"final_answer" — so providers send it. opencode drops it: OpenAIResponsesStreamItem (packages/llm/src/protocols/openai-responses.ts:177-198) enumerates item fields without phase, and rg -w -i 'phase' packages/llm/src is empty. (phase does occur elsewhere in the tree — cli/cmd/run/*, UI and e2e code — but that is opencode's own scrollback/UI lifecycle state, never parsed from a model-emitted field.)


Plugins

None. (Default configuration, no plugins loaded.)


OpenCode version

v1.18.27 (desktop Electron build; the bundled server reports 1.18.26).


Steps to reproduce

Reproducible against a local stub provider — no real model needed.

  1. Configure an OpenAI-compatible chat-completions model whose id contains gpt and does not match earlier prompt branches (gpt-4, o1, o3, codex), e.g. gpt-5.5. system.ts:32-33 pre-empts gpt-4/o1/o3 to PROMPT_BEAST and :35-37 sends codex ids to PROMPT_CODEX; everything else matching gpt falls through to PROMPT_GPT at :38.
  2. Point opencode at a local stub endpoint serving that model id.
  3. Dump the system prompt and confirm the "## Response channels" block from gpt.txt:81 is present. Nothing about the provider's transport is consulted.
  4. Send one prompt that needs several tool calls. Hold everything constant and vary only what the stub streams for the first assistant turn:
stub streams tool parts finish result
text "Reading the config now.", then finish_reason: "stop" none stop guard at prompt.ts:1113 fires, exiting loop logged at :1128, loop breaks at step 1
the same text plus one tool_calls delta, then finish_reason: "tool_calls" one tool-calls loop continues (control)

Row 1 is the message shape gpt.txt:95 instructs the model to send before starting work. Row 2 is the only reason the agent normally survives a progress update: the model happened to attach a tool call to it.


Screenshot and/or share link

No share link — the affected sessions run against a self-hosted gateway that is not reachable from /share. The behaviour is reproducible from the stub-provider recipe above without one.


Operating System

Linux (Ubuntu, kernel 6.8), x86_64.


Terminal

Not applicable — observed in the desktop (Electron) app, not a terminal. The mechanism is in the server-side agent loop and is transport-, not terminal-, dependent.

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.