reasoning: long reasoning is typed as text and leaks the closing </think> delimiter · Issue #56428 · anomalyco/opencode · GitHub
Skip to content

reasoning: long reasoning is typed as text and leaks the closing </think> delimiter #56428

Closed as duplicate

Description

@Bad0Weather

Summary

Assistant reasoning is classified inconsistently. Short reasoning is stored/rendered as a reasoning content part, but longer reasoning is fragmented into text parts and the reasoning→answer delimiter leaks into the visible assistant text. The opening delimiter never appears, so a long reasoning block shows only its closing delimiter.

Environment

  • opencode version: v2.0.31 (Arch package opencode 2.0.31-1)
  • OS: Linux 7.2.10-arch1-1 (Arch Linux), x86_64
  • Terminal: TERM=xterm-256color, COLORTERM=truecolor (alacritty)
  • Shell: /usr/bin/bash
  • Install/channel: Arch Linux package (extra), upstream https://github.com/anomalyco/opencode
  • Active plugins: none found (no ~/.config/opencode/plugin or plugins dir; opencode.jsonc contains only $schema)

Model used in the affected session: provider opencode, model big-pickle, variant default, agent build.

Reproduction

  1. Start a session with a model that emits reasoning (here: opencode/big-pickle).
  2. Send a prompt that yields a short reasoning block (a few hundred chars).
  3. Send a prompt that yields a long reasoning block (several KB).
  4. Observe the assistant output, and/or inspect ~/.local/share/opencode/opencode.db.

Inspect with (read-only copy recommended):

sqlite3 opencode.db "SELECT sm.seq, json_extract(j.value,'$.type'),
  length(json_extract(j.value,'$.text'))
FROM session_message sm, json_each(sm.data,'$.content') j
WHERE sm.session_id='<session>' AND sm.type='assistant';"

Expected Behavior

Reasoning should be stored/rendered as a single reasoning unit delimited by ... , for any length. The closing delimiter should be consumed at the reasoning→answer boundary and never appear in the assistant's visible text. The delimiters are the byte sequences:

  • open: 3c 74 68 69 6e 6b 3e
  • close: 3c 2f 74 68 69 6e 6b 3e

i.e. .

Actual Behavior

Short reasoning blocks are correct, long ones are not.

  • Short turns produce a clean reasoning part (no delimiters), e.g.:
    • seq 7: reasoning:41
    • seq 20: reasoning:219 + clean text:217
  • Long turns produce a single text part whose body is <reasoning text></think><answer text>, with no opening delimiter, e.g.:
    • seq 35: text:1095, close delimiter at byte offset 891
    • seq 48: text:195, close delimiter at offset 80
    • seq 57: text:2230, close delimiter at offset 1509
  • Some long turns are split into multiple parts of the same reasoning stream with inconsistent types, e.g. seq 459: text:7838 followed by reasoning:574.
  • There are parts that consist of nothing but the closing delimiter: seq 92 and seq 174 are 9-byte text parts equal to plus a newline.
  • The opening delimiter appears as the first bytes of no content part (0 occurrences).

Counts for the affected session ses_eb6044450ffeMO126Z5pJ5rhhK:

metric value
assistant messages 63
... containing the close delimiter 61
... containing the open delimiter 12 (quotations inside reasoning text, not structural)
content parts total 134
parts typed reasoning 8
parts typed text containing the close delimiter 61
close-only (text = delimiter only) parts 3
parts beginning with the open delimiter 0

Global (all sessions in the DB): 364 parts typed reasoning vs 118 parts typed text containing the closing delimiter.

Net effect: for a long reasoning block the opening delimiter is gone while the closing delimiter remains, so the UI shows a dangling close marker (and the reasoning is surfaced as normal assistant text instead of a reasoning block). For short blocks both delimiters are handled and it renders correctly.

Additional Context

  • The stored form of a broken turn is one text part: <reasoning text></think><answer text>. The closing delimiter sits exactly on the reasoning→answer boundary (verified byte offsets above), but the reasoning before it is typed text rather than reasoning.
  • The open delimiter (hex 3c 74 68 69 6e 6b 3e) never appears on the assistant side; only the close delimiter (hex 3c 2f 74 68 69 6e 6b 3e) does.
  • Working vs broken correlates with block size: the only correctly split reasoning parts in the session are the two shortest (41 and 219 chars); every larger block stayed as text with the leaked delimiter. This looks like delimiter handling that succeeds for a small/single-chunk block but fails once reasoning spans multiple stream chunks.
  • No plugins are configured, so this is not plugin-induced.
  • DB inspected read-only at ~/.local/share/opencode/opencode.db (session_message.data.content[]).

Activity

  1. opencode-agent commented on Oct 17, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the byte-level measurements, @Bad0Weather.

    I think this is a duplicate of #54599. In both, opencode/big-pickle sends its reasoning inline in the reply, ending in a literal </think> with no opening tag, and OpenCode saves it as a normal text part instead of a reasoning part. Comments there describe the same pattern you saw: some turns get a proper reasoning part and others leak, and the opening tag never shows up. PR #54612 proposes a fix that splits the inline reasoning out where the stream is parsed.

    Possible duplicates and related issues, ordered by likelihood:

    Your per-session counts (short turns correct, long turns leaking) would be useful on #54599, so please add them there as a comment.

    The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.

  2. Bad0Weather commented on Oct 17, 2026

    @Bad0Weather
    Author

    Adding the per-session counts from my duplicate report (#56428), as requested.

    Environment: opencode v2.0.31 (Arch Linux), model opencode/big-pickle (provider opencode, variant default, agent build).
    Session: ses_eb6044450ffeMO126Z5pJ5rhhK. Measured on session_message.data.content[] (V2 schema).

    Counts

    metric value
    assistant messages 63
    ... containing the close marker 61
    ... containing the open marker 12 (all quotations inside CoT text, not structural)
    content parts total 134
    parts typed reasoning 8
    parts typed text containing the close marker 61
    parts that are only the close marker (9 bytes: marker + \n) 3
    parts whose first bytes are the open marker 0

    Short turns correct, long turns leak

    In this session the split works only for the very smallest CoT blocks; everything larger leaks. Stored shape of a leaked turn is a single text part: <CoT></think><answer> — the close marker sits exactly on the CoT→answer boundary, but the CoT before it is typed text, and there is no opening marker anywhere.

    seq content parts close-marker byte offset result
    7 reasoning:41 — correct (collapsible reasoning part)
    20 reasoning:219, text:217 — correct
    48 text:195 80 leaked
    35 text:1095 891 leaked
    57 text:2230 1509 leaked

    So the mid-session flip others described shows up here as a size threshold: seq 7 and 20 (CoT 41 / 219 chars) parse as reasoning; from seq 35 onward (CoT ≥ ~891 chars) nothing does, and the close marker leaks into text.

    Inconsistent split within a single turn

    seq 459 has text:7838 immediately followed by reasoning:574 — two consecutive chunks of the same CoT stream typed differently. Small reasoning fragments also appear on otherwise-leaked turns (seq 279:5, seq 430:9, seq 591:52), which look like parser artifacts rather than real reasoning parts.

    Global

    Across all sessions in the same DB: 364 parts typed reasoning vs 118 parts typed text containing the close marker.

    Happy to attach the raw sqlite3 output / a session export if useful.

  3. opencode-agent commented on Oct 17, 2026

    @opencode-agent
    Contributor

    Thanks for the extra measurements, @Bad0Weather. The size threshold (short CoT parsed as reasoning, longer CoT leaking into text with </think> at the boundary) and the mixed types within one turn in seq 459 are useful details.

    It looks like this comment was posted here on #56428, not on #54599. That issue is where the fix is being tracked and where PR #54612 is being reviewed. Could you post the same comment there so it reaches the people working on it? Raw sqlite3 output or a session export would also be most useful on #54599.

    Nothing in these numbers suggests a different problem from #54599, so the bot will still close this issue as a duplicate as planned.

  4. opencode-agent commented on Oct 18, 2026

    @opencode-agent
    Contributor

    Closing as a duplicate of #54599, since nothing here points to a different problem. Both are about opencode/big-pickle sending its reasoning inline with a literal </think> and no opening tag, which OpenCode then saves as visible text instead of a reasoning part. The proposed fix is PR #54612.

    Thanks again for the detailed counts, @Bad0Weather. The size threshold and the mixed types within one turn in seq 459 would help most on #54599, so please add them there if you haven't yet. If this turns out to be a different problem, reply here and we can reopen.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions