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
Either the < or <= operators are causing responses from the ai to get cut off. #23928
This has been a recurring issue I've noticed for a bit now, and I think I've finally tracked down at least part of the cause.
Every once in a while in the response from the agent, it would seemingly randomly get cut off midway through outputing something. I noticed showing the ai "thinking" would never cause a problem, as well as the ai outputting shell commands. but during the actual response. It just get's cut off.
I also noticed, it almost always seemed to happen white it outputs either an if statement, or a loop of some kind (I'm writing c, but really I don't think that matters). Eventually I was able to see what it was trying to output directly when it stopped. tell it to try testing if that's what keeps stopping it, and outputting <= seemed to stop it every time before it could output the text regardless of what it was doing.
My current theory is that somehow <= or < is interacting with terminal escape sequences, and that is somehow causing it to stop. That or their is something broken within opencodes system that causes it to stop when those specific charecters are sent in a response. But whatever it is it's for sure some interaction going on with the < charecter specifically.
Sorry if this happens to be a duplicate, there are 4000 issues open it appears so there definitely isn't going to be any way for me to look through all of those.
Also one extra detail, I use st for my terminal normally. but the same issue happens when using kitty, and I believe foot as well. So I don't think it's the terminal that's causing it.
Plugins
none as far as I know
OpenCode version
1.14.20 (but this problem has persisted across versions)
Steps to reproduce
have the ai output code, or a response with <= in it.
This looks like a downstream symptom of two upstream @opentui/coreCodeRenderable streaming bugs altendky filed yesterday:
anomalyco/opentui#963: in the content setter, the _streaming && !_drawUnstyledText && _filetype branch stores the new content and returns without calling requestRender(). If the renderer is otherwise idle, no frame is ever scheduled and the text buffer holds stale content until something external (e.g. keypress) kicks a frame.
anomalyco/opentui#964: startHighlight() has four snapshot-mismatch early returns that also don't call requestRender(), so when newer content arrives faster than the async highlighter finishes, the aborted highlight leaves the renderer unscheduled.
Why < / <= specifically trigger it is consistent with (but not proven by) this mechanism: tree-sitter-markdown-inline treats < as the start of an autolink or inline HTML tag and has to disambiguate based on what follows. A lone <= isn't valid for either shape, so the parse likely backtracks, lengthening the highlight-in-flight window and giving newer stream chunks more chance to win the snapshot race in #964. I didn't reproduce this in isolation, so this is a fit with the upstream root-cause, not a confirmed cause.
Two consistent observations from the report:
Shell commands don't trigger it: ToolPart dispatches to per-tool renderers (Bash, Glob, Read, etc.) and bypasses CodeRenderable streaming entirely.
Terminal-independent (st / foot / kitty): consistent with the bug being in the render-scheduling layer rather than in escape-sequence handling.
Weak spot: reasoning uses the same <code streaming filetype=markdown> wrapper at ReasoningPart:1458-1466 and the reporter says it's fine. If the mechanism is right, the difference is likely timing (shorter reasoning streams, or less code-heavy content giving less tree-sitter backtracking), but I don't have a clean read on that.
If this fit holds, opencode#23928 can track through the two upstream issues rather than be fixed here. A defensive requestRender() call somewhere in the opencode TextPart boundary would be a workaround but the clean fix is in CodeRenderable.
i tried this locally and im pretty sure the problem isnt just the opencode feature flag or the terminal itself, it feels like something in the markdown rendering path
what i saw while testing
if i render a < b using the experimental markdown path it breaks
if i render the same thing using the fallback code filetype markdown path it also breaks
but if i just print it as plain text its fine
so yeah the experimental flag only changes which path gets used, but both markdown related paths still have the issue
not 100 percent sure where exactly its breaking, but it feels like its happening a bit deeper, maybe somewhere in the markdown or rendering layer inside @OpenTui core rather than in opencode itself
That triangulation narrows it well. The common factor across both paths is CodeRenderable with streaming:true, drawUnstyledText:false, filetype:"markdown".
The experimental <markdown> path maps to MarkdownRenderable, which internally constructs CodeRenderable via createMarkdownCodeRenderable in the @opentui/core 0.1.99 bundle:
And applyMarkdownCodeRenderable sets renderable.content = content on the existing CodeRenderable, which is the exact setter branch from anomalyco/opentui#963 that early-returns without requestRender().
The fallback <code filetype="markdown" streaming={true} drawUnstyledText={false}> path at packages/opencode/src/cli/cmd/tui/routes/session/index.tsx:1488-1496 is the same CodeRenderable config directly.
Plain text being fine is consistent: no CodeRenderable, no _filetype, no stalling branch.
Both paths converge on the same upstream bug. The fix site is CodeRenderable.content setter and startHighlight() in opentui, not anything in opencode.
opencode v1.14.27 bumped @opentui/core to 0.1.105 (commit a3128e3, ~17h ago), which is the first release line that ships the fix from anomalyco/opentui#965 — "add missing requestRender() in CodeRenderable streaming content setter," merged as 5e20a2e and tagged in opentui 0.1.103.
@mrrewilh, what does opencode --version print? If it's v1.14.27 or later and the bug still reproduces with Observable<...> / Option<...> truncation, the upstream patch isn't sufficient and there's a second code path missing a render trigger.
opencode v1.14.27 bumped @opentui/core to 0.1.105 (commit a3128e3, ~17h ago), which is the first release line that ships the fix from anomalyco/opentui#965 — "add missing requestRender() in CodeRenderable streaming content setter," merged as 5e20a2e and tagged in opentui 0.1.103.
@mrrewilh, what does opencode --version print? If it's v1.14.27 or later and the bug still reproduces with Observable<...> / Option<...> truncation, the upstream patch isn't sufficient and there's a second code path missing a render trigger.
Can confirm after the update everything works perfectly again.
Thanks for the follow-up. The fix from anomalyco/opentui#965 covered the missing requestRender() in the _streaming && !_drawUnstyledText && _filetype branch. The "works for a while, then happens again" pattern reads like a second code path missing a similar render trigger rather than a regression of the original bug.
To triangulate, four pieces:
opencode --version (confirm current build — v1.14.27 through v1.14.29 all pin @opentui/core@0.1.105; opentui's v0.1.106 and v0.2.0 haven't shipped any CodeRenderable changes since the Explain usage of external references in AGENTS.md #965 fix).
The actual cut-off text as text rather than a screenshot, with the line it stops at.
Which model you're using (Claude / GPT / Gemini stream chunks differently and may exercise different paths).
Whether experimental.markdown = true is set, or you're on the fallback code filetype path.
If the cut-off line consistently ends at the same kind of token (e.g. always inside < / <= / a code-fence open), that points the second missing render trigger at the same lexer-state branch. Different tokens each time suggests a timing-only race.
Thanks for the follow-up. The fix from anomalyco/opentui#965 covered the missing requestRender() in the _streaming && !_drawUnstyledText && _filetype branch. The "works for a while, then happens again" pattern reads like a second code path missing a similar render trigger rather than a regression of the original bug.
To triangulate, four pieces:
* `opencode --version` (confirm current build — v1.14.27 through v1.14.29 all pin `@opentui/core@0.1.105`; opentui's `v0.1.106` and `v0.2.0` haven't shipped any CodeRenderable changes since the [Explain usage of external references in AGENTS.md #965](https://github.com/anomalyco/opencode/pull/965) fix).
* The actual cut-off text as text rather than a screenshot, with the line it stops at.
* Which model you're using (Claude / GPT / Gemini stream chunks differently and may exercise different paths).
* Whether `experimental.markdown = true` is set, or you're on the fallback `code` filetype path.
If the cut-off line consistently ends at the same kind of token (e.g. always inside < / <= / a code-fence open), that points the second missing render trigger at the same lexer-state branch. Different tokens each time suggests a timing-only race.
opencode version:
❯ opencode --version
1.14.29
Model: MiniMax M2.5 Free
experimental.markdown: default one i didnt edit any json or config.
Cut-off output:
Let me create a comprehensive plan. // This is the thinking part (the gray area)
Based on research, here's my plan:
Plan: Core Search, Filter & VRDB Integration
Current VR Src Data (already in core)
Field
name, package_name
version, size_mb, downloads
rating, rating_count
Missing from VR Src: developer, genres, description, images, languages, comfort level
Thanks for the version and cut-off text. Two updates from my earlier note:
Correction:OPENCODE_EXPERIMENTAL_MARKDOWN defaults to true — the flag uses !falsy(...) in packages/core/src/flag/flag.ts, so an unset env var resolves to true. At default config you're rendering through MarkdownRenderable, not the <code filetype="markdown"> fallback. The fix in opentui#965 was on CodeRenderable.content setter, which is a different layer than the one your repro is exercising. That was my read miss.
MarkdownRenderable parses incrementally and creates child renderables per block (one CodeRenderable per markdown block, separate ones for fenced code). With streaming={true} it keeps the trailing 2 tokens "unstable" until more content arrives. The TextPart in opencode hardcodes streaming={true} and never flips it to false, so at end-of-stream the trailing unstable tokens stay unstable indefinitely.
One focused diagnostic to split where the text went:
After a cut-off, does pressing any key or scrolling refresh the display to show the missing text?
Yes → text reached the buffer but the final paint never happened (missing render trigger on the trailing-unstable boundary, or in a child renderable).
No → text never made it into the buffer (parser dropped it, or set content short-circuited somewhere).
If you can also paste the complete text after streaming finishes (let it sit a moment, then copy from the TUI scrollback), that distinguishes "final display cut off" from "final buffer cut off".
No, I'm one million percent sure i can't see missing text. For example;
VRDB URL pattern: https://vrdb.app/game/{slug}/{id}
- Example: https://vrdb.app/game/beat-saber/2448060205267927
---
Implementation TODO List (For Myself)
┌─────────────────────────────────────────────────────────────┐
│ TASK 1: Extend Game Model │
├─────────────────────────────────────────────────────────────┤
- Add vrdb_id: Option
▣ Plan · MiniMax M2.5 Free · 41.0s
This was copied from TUI, after i left session with exit command then, continued with opencode -s ses_bla_bla
Also in case if it matters, first it stops for a second. Like something happened (?) then it ends. @truffle-dev
The SQL output settles where the content goes: storage row matches the TUI cut-off character-for-character, so the stream/storage write path isn't dropping chunks — opencode is persisting exactly what the model returned, and rendering exactly that.
What I think is happening on those plans: the cut-off site (fn fetch_game(&self, game_id: &str mid-line, mid-token) is the shape you'd expect when the model hits its output-token limit. MiniMax M2.5 Free's per-request output cap is well under opencode's 32k default, so on a long plan the response truncates mid-content and the stream finishes with finishReason: "length". opencode then writes message.finish = "length", the message is treated as final, and the TUI shows the truncated text without surfacing the cause. The "stops for a second, then ends" pause matches the provider closing the stream after the limit is reached, not a render race.
There's nothing on the opencode side that can recover the missing tokens (the model never emitted them), but the cause should at least be visible. Opened #25557 to add a non-stop finish-reason indicator next to the existing · interrupted suffix in the assistant header — for length / content-filter / error you'd see e.g. ▣ Plan · MiniMax M2.5 · 41.0s · truncated by length limit. Same suffix is added to the markdown transcript export.
Workaround on your side until/if MiniMax raises the free-tier cap: switch to a model with a higher output cap for long plans (Claude / Sonnet, Gemini 2.5 Pro, GPT-4o all have higher per-request output budgets than MiniMax M2.5 Free), or shorten the prompt so the response fits.
After the latest cut-off, i prompted minimax to continue.
VRDB implemented as swappable module - not coupled to VRSrc:
pub trait MetadataSource {
fn fetch_game(&self, game_id: &str
▣ Plan · MiniMax M2.5 Free · 3m 12s
continue. Message got cutted off. // my prompt
Thinking: The user wants me to continue the implementation plan. Let me continue from where I left off - I need to finish documenting the VRDB metadata fields, download strategy, and create the full TODO list.
Since I'm in read-only/planning mode, I should just complete the plan document and present it to the user for their approval.
I'll complete the implementation plan now:
---
3. Download Strategy (via @explore agent research)
Recommended: ugi crate
Feature Status
Parallel chunk downloads ✅ HTTP Range requests
Resume capability ✅ ETag/Last-Modified
Rate limiting ✅ Token-bucket
Integrity verification ✅ SHA256, Blake3
Progress callbacks ✅ For UI
Cross-platform ✅ Linux/Windows
No external binaries ✅ Pure Rust
Runtime ✅ Tokio-compatible
Alternative: Custom reqwest implementation if more control needed.
---
4. Archive Extraction
Already implemented with sevenz-rust:
sevenz_rust::decompress_file_with_password(archive_path, output_dir, password.into());
Supports:
- ✅ Password-protected .7z archives
- ✅ AES-256 encryption
- ✅ Cross-platform (pure Rust)
---
5. Full Implementation TODO
# Task Priority Notes
1 Add VRSrc search to Game model search(query) → `
▣ Plan · MiniMax M2.5 Free · 8.9s
As you can see, it cuts on short prompt and even short answer.
Query for this session;
[rewilh@buzul-x8664 ~]$ opencode db "SELECT json_extract(data, '$.text') AS text FROM part WHERE session_id = 'ses_235d4daafffedAM5H1sjFYCDVN' AND json_extract(data, '$.type') = 'text' ORDER BY id DESC LIMIT 3" --format json
[
{
"text": "\n\nI'll complete the implementation plan now:\n\n---\n\n### 3. Download Strategy (via @explore agent research)\n\n**Recommended: `ugi` crate**\n\n| Feature | Status |\n|---------|--------|\n| Parallel chunk downloads | ✅ HTTP Range requests |\n| Resume capability | ✅ ETag/Last-Modified |\n| Rate limiting | ✅ Token-bucket |\n| Integrity verification | ✅ SHA256, Blake3 |\n| Progress callbacks | ✅ For UI |\n| Cross-platform | ✅ Linux/Windows |\n| No external binaries | ✅ Pure Rust |\n| Runtime | ✅ Tokio-compatible |\n\n**Alternative:** Custom reqwest implementation if more control needed.\n\n---\n\n### 4. Archive Extraction\n\nAlready implemented with `sevenz-rust`:\n\n```rust\nsevenz_rust::decompress_file_with_password(archive_path, output_dir, password.into());\n```\n\nSupports:\n- ✅ Password-protected .7z archives\n- ✅ AES-256 encryption\n- ✅ Cross-platform (pure Rust)\n\n---\n\n### 5. Full Implementation TODO\n\n| # | Task | Priority | Notes |\n|---|------|----------|-------|\n| 1 | Add VRSrc search to Game model | `search(query)` → `"
},
{
"text": "continue. Message got cutted off."
},
{
"text": "\n\n---\n\n## Implementation Plan: Game Search, Metadata, and Download\n\n### 1. VRSrc Game List Audit\n\n**Current Structure (GameList.txt - 9 fields):**\n\n| # | Field | Example |\n|---|-------|---------|\n| 1 | Game Name | `10 Seconds` |\n| 2 | Release Name | `10 Seconds v3+1.0 -VRP v76` |\n| 3 | Package Name | `com.ZynkSoftware.SecondsMatter` |\n| 4 | Version Code | `3` |\n| 5 | Last Updated | `2026-04-15 20:45 UTC` |\n| 6 | Size (MB) | `107` |\n| 7 | Downloads | `83.84` |\n| 8 | Rating | `0` |\n| 9 | Rating Count | `0` |\n\n**Status:** Minimal metadata - missing description, genres, tags, comfort level, languages, etc.\n\n---\n\n### 2. VRDB Integration\n\n**URL Pattern:** `https://vrdb.app/game/{slug}/{id}`\n\n**Example:** `https://vrdb.app/game/beat-saber/2448060205267927`\n\n**Available Metadata Fields:**\n\n| Category | Fields |\n|----------|--------|\n| **Core** | Name, Description, Genres, Release Date, Developer, Publisher |\n| **Platform** | Platforms (Quest 2/3/Pro/3S), Cross-Buy, Mixed Reality |\n| **Ratings** | Rating (1-5★), Rating Count, Review Text |\n| **Technical** | Version, Supported Controllers, Player Modes, Languages |\n| **Comfort** | Comfort Level (Comfortable → Intense), Motion Sickness |\n| **Content** | Tags, Game Modes, Playtime Estimates |\n| **Pricing** | Current Price, Price History |\n\n**Architecture (per AGENTS.md):**\n\n```\nsources/\n├── mod.rs # Source trait\n├── telegram.rs # VRSrc (existing) \n└── vrdb.rs # NEW - VRDB as separate module\n```\n\nVRDB implemented as **swappable module** - not coupled to VRSrc:\n\n```rust\npub trait MetadataSource {\n fn fetch_game(&self, game_id: &str"
}
]
[rewilh@buzul-x8664 ~]$
The continue-attempt reproducing the cut-off (and storage matching the TUI again) is good signal. It rules out "first response was unlucky" and points at a stable mechanism. It also undermines my earlier "model hit length" framing: both the long initial response and the short continue response cut off, which is the wrong shape for a fixed output-token budget.
The next diagnostic lives one table up. part only stores the textual content; the finish reason and any error live on message.data. If you can run:
SELECT
json_extract(data, '$.role') AS role,
json_extract(data, '$.finish') AS finish,
json_extract(data, '$.error') AS error,
json_extract(data, '$.tokens.output') AS output_tokens,
json_extract(data, '$.cost') AS cost
FROM message
WHERE session_id ='ses_235d4daafffedAM5H1sjFYCDVN'AND json_extract(data, '$.role') ='assistant'ORDER BY id DESCLIMIT5
Three branches from the result:
finish = "length": model self-reported hitting its output cap. PR fix(tui): surface non-stop finish reasons in assistant header #25557 surfaces this in the TUI as · truncated by length limit. Underlying ask is "raise the model's output limit" (for MiniMax via OpenRouter, that's the upstream provider's max_tokens default).
finish is null/empty on the truncated rows: most likely the SSE stream was cut before any finish-step event arrived. opencode sets assistantMessage.finish only when it receives a finish-step event from the model (packages/opencode/src/session/processor.ts:470); the only fallback to "stop" sits in the structured-output branch (prompt.ts:1591). A network-level disconnect on free-tier MiniMax leaves finish undefined, the message renders without any suffix, and the truncation is invisible. PR fix(tui): surface non-stop finish reasons in assistant header #25557 does NOT yet cover this case.
The "even short prompts and short answers" datapoint plus the consistent cut-offs really point at branch three. If your query confirms finish is null on the truncated rows, I'll send a follow-up tracking whether a finish-step ever arrived and surfacing · stream cut in the TUI for that case.
Thanks for the query. The data rules out all three branches I pointed at: no length, no error, no null finish. Two rows ended stop (742 and 1194 output tokens), three ended tool-calls (65, 187, 331). The model is self-reporting normal completion every time. PR #25557 only attaches a suffix when finish is length, error, or content-filter, so it won't surface anything for these rows. The follow-up patch I floated for "stream cut before finish-step" doesn't apply either, because the finish event clearly arrived.
That changes the question. Two failure modes still fit the "feels cut off" symptom:
Model-quality terseness on the stop rows. 742 visible tokens is roughly 500-800 words, substantive, but free-tier MiniMax sometimes wraps a complex ask in a short summary instead of working through it. If the truncated message you screenshotted earlier is one of these stop rows, this is upstream behavior, not opencode dropping content. The diagnostic: pick the row with finish="stop" and output_tokens=742, run
SELECT json_extract(data, '$.text') AStext, length(json_extract(data, '$.text')) AS chars
FROM part
WHERE message_id = (SELECT id FROM message WHERE session_id='ses_235d4daafffedAM5H1sjFYCDVN'AND json_extract(data,'$.tokens.output')=742ORDER BY id DESCLIMIT1)
AND json_extract(data,'$.type')='text';
If the text part holds a substantial response (a few thousand chars), the model wrote what it intended and the cut-off is perceptual. If the text part is much shorter than 742 tokens would predict, something is dropping content between the SSE stream and the part write. That's a real opencode bug worth tracking separately.
Tool-loop interruption on the tool-calls rows. Each finish="tool-calls" row should be followed by tool execution and another assistant step. If the next step never fires (free-tier rate-limit between turns, OpenRouter reconnect, etc.), the conversation stalls on a short message and looks cut off. To check, take the most recent finish="tool-calls" row's id and run
SELECT json_extract(data,'$.role') AS role, json_extract(data,'$.finish') AS finish, id
FROM message
WHERE session_id='ses_235d4daafffedAM5H1sjFYCDVN'AND id ><that_id>ORDER BY id ASCLIMIT5;
If there's no follow-up assistant or tool message, the tool loop bailed silently after the model emitted calls. That's a different fix path (heartbeat or retry on tool-loop continuation), not a finish-suffix.
Either result narrows the next move. Apologies for the wrong branches in the first reply. Your continue-attempt evidence pushed me one table up, and the data pushed me one layer further than that.
Hypothesis (a) confirmed. The stop row reports 172 output tokens. English averages roughly 4 chars per token, so 172 tokens predicts something around 600 to 800 chars. The stored text part is 296 chars and ends in the middle of a word (Fixed Telegram HTML parsing (uses). That mid-word terminator is the smoking gun. If the model had naturally stopped there, the last token would be a punctuation or sentence end, not an open paren followed by a verb.
Hypothesis (b) ruled out. Every assistant row in query-7.json has a populated finish (one stop plus nine tool-calls). The tool loop is advancing normally, no silent bail.
So the question is where the drop happens between the model emitting and the part being saved. opencode's text accumulator is at packages/opencode/src/session/processor.ts:533-544:
It just concatenates whatever arrives, no filtering. So the deltas opencode sees are the deltas the AI SDK demuxed from the SSE stream, which are the deltas the OpenRouter free-tier proxy forwarded from MiniMax. The mid-word cut plus the finish="stop" event arriving cleanly afterward fits one shape best: the upstream proxy truncated the model's actual output and emitted a synthetic finish_reason="stop" event. From opencode's perspective the stream looked complete, so it stored what it got and rendered no truncation suffix.
The original < / <= correlation may be a related but distinct symptom of the same upstream behavior. The current session's truncated text contains no <, and it still cuts mid-word. Whatever the proxy is doing, it's not strictly conditional on those characters.
Two opencode-side fixes worth considering separately from PR #25557:
Heuristic mismatch warning. When output_tokens exceeds length(text part) / 3 by some margin (say 2x), surface · response may be truncated upstream in the assistant header. Catches synthesized-stop cases that PR fix(tui): surface non-stop finish reasons in assistant header #25557's length/error/content-filter switch can't see.
Stream-event delta count audit. Add an opt-in debug log that records the count of text-delta events received and the cumulative chars per part. If the count looks plausible (e.g. 50 deltas averaging ~6 chars) the truncation is upstream; if there's a sudden gap (say 50 deltas then nothing for 10s before finish-step), the truncation is between AI SDK and opencode.
Happy to ship either if the maintainers want one. The first is a small TUI patch on top of #25557; the second is a few logs in processor.ts behind a DEBUG=opencode:stream flag.
For your immediate situation: a paid-tier OpenRouter key or a different provider would test the upstream-truncation theory directly. If the same prompt completes cleanly on a non-free MiniMax key (or any other model), the proxy is the culprit and the fix path is upstream. If it still truncates everywhere, that's stronger signal for an opencode-side stream parsing issue and the audit log would catch it.
Sounds good. To leave a useful breadcrumb for anyone who lands here later: the diagnostic narrowed to opencode receiving stream events that look complete (text-deltas + a clean finish-step with reason="stop"), but the stored text is shorter than output_tokens predicts and ends mid-word. That fits an upstream proxy synthesizing a stop event after truncating the model's actual output, with opencode faithfully recording what arrived.
Two opencode-side fixes that would surface this to users (separate from #25557, which only catches length/error/content-filter) if maintainers want them as actionable items:
Heuristic mismatch warning in the assistant header when output_tokens exceeds length(text) / 3 by a margin (catches synthesized-stop cases).
Opt-in DEBUG=opencode:stream log of text-delta event count and cumulative chars per part (lets a user distinguish upstream truncation from any AI-SDK-to-opencode delta drop).
Happy to ship either as a follow-up PR if there's interest. Otherwise leaving the diagnostic here so the next person hitting this can skip the four rounds.
i haven't been noticing it lately. so it seems to have been resolved at some point. Not sure if it was fixed by some unrelated change, or something earlier after I reported the issue.
while the issue was happening, it definitely wasn't getting cut off because of length. it would get caught off sometimes on the first sentence if i couldn't get the model to start again on the line after the > (or it might have been < ) occured.
But like I said, it seems to have been resolved as of now.
Activity
truffle-dev commentedon Apr 23, 2026
This looks like a downstream symptom of two upstream
@opentui/coreCodeRenderablestreaming bugs altendky filed yesterday:contentsetter, the_streaming && !_drawUnstyledText && _filetypebranch stores the new content and returns without callingrequestRender(). If the renderer is otherwise idle, no frame is ever scheduled and the text buffer holds stale content until something external (e.g. keypress) kicks a frame.startHighlight()has four snapshot-mismatch early returns that also don't callrequestRender(), so when newer content arrives faster than the async highlighter finishes, the aborted highlight leaves the renderer unscheduled.Code path in opencode HEAD
871789ce64f179d5efa3031afbce3789e27e99b5:TextPartatpackages/opencode/src/cli/cmd/tui/routes/session/index.tsx:1470-1500renders assistant text through<code filetype="markdown" drawUnstyledText={false} streaming={true}>, which maps toCodeRenderablewith exactly the_streaming && !_drawUnstyledText && _filetypecombination from #963.Why
</<=specifically trigger it is consistent with (but not proven by) this mechanism:tree-sitter-markdown-inlinetreats<as the start of an autolink or inline HTML tag and has to disambiguate based on what follows. A lone<=isn't valid for either shape, so the parse likely backtracks, lengthening the highlight-in-flight window and giving newer stream chunks more chance to win the snapshot race in #964. I didn't reproduce this in isolation, so this is a fit with the upstream root-cause, not a confirmed cause.Two consistent observations from the report:
ToolPartdispatches to per-tool renderers (Bash, Glob, Read, etc.) and bypassesCodeRenderablestreaming entirely.Weak spot: reasoning uses the same
<code streaming filetype=markdown>wrapper atReasoningPart:1458-1466and the reporter says it's fine. If the mechanism is right, the difference is likely timing (shorter reasoning streams, or less code-heavy content giving less tree-sitter backtracking), but I don't have a clean read on that.If this fit holds, opencode#23928 can track through the two upstream issues rather than be fixed here. A defensive
requestRender()call somewhere in the opencode TextPart boundary would be a workaround but the clean fix is inCodeRenderable.kzekiue commentedon Apr 23, 2026
i tried this locally and im pretty sure the problem isnt just the opencode feature flag or the terminal itself, it feels like something in the markdown rendering path
what i saw while testing
so yeah the experimental flag only changes which path gets used, but both markdown related paths still have the issue
not 100 percent sure where exactly its breaking, but it feels like its happening a bit deeper, maybe somewhere in the markdown or rendering layer inside @OpenTui core rather than in opencode itself
truffle-dev commentedon Apr 23, 2026
That triangulation narrows it well. The common factor across both paths is
CodeRenderablewithstreaming:true, drawUnstyledText:false, filetype:"markdown".The experimental
<markdown>path maps toMarkdownRenderable, which internally constructsCodeRenderableviacreateMarkdownCodeRenderablein the @opentui/core 0.1.99 bundle:And
applyMarkdownCodeRenderablesetsrenderable.content = contenton the existingCodeRenderable, which is the exact setter branch from anomalyco/opentui#963 that early-returns withoutrequestRender().The fallback
<code filetype="markdown" streaming={true} drawUnstyledText={false}>path atpackages/opencode/src/cli/cmd/tui/routes/session/index.tsx:1488-1496is the sameCodeRenderableconfig directly.Plain text being fine is consistent: no
CodeRenderable, no_filetype, no stalling branch.Both paths converge on the same upstream bug. The fix site is
CodeRenderable.contentsetter andstartHighlight()in opentui, not anything in opencode.mrrewilh commentedon Apr 27, 2026
Add to models/game.rs → extend Catalog:
This happens on almost every message.
truffle-dev commentedon Apr 27, 2026
opencode v1.14.27 bumped
@opentui/coreto 0.1.105 (commita3128e3, ~17h ago), which is the first release line that ships the fix from anomalyco/opentui#965 — "add missingrequestRender()in CodeRenderable streaming content setter," merged as5e20a2eand tagged in opentui 0.1.103.@mrrewilh, what does
opencode --versionprint? If it's v1.14.27 or later and the bug still reproduces withObservable<...>/Option<...>truncation, the upstream patch isn't sufficient and there's a second code path missing a render trigger.mrrewilh commentedon Apr 27, 2026
Can confirm after the update everything works perfectly again.
mrrewilh commentedon Apr 28, 2026
No, out of nowhere it happened again.
truffle-dev commentedon Apr 28, 2026
Thanks for the follow-up. The fix from anomalyco/opentui#965 covered the missing
requestRender()in the_streaming && !_drawUnstyledText && _filetypebranch. The "works for a while, then happens again" pattern reads like a second code path missing a similar render trigger rather than a regression of the original bug.To triangulate, four pieces:
opencode --version(confirm current build — v1.14.27 through v1.14.29 all pin@opentui/core@0.1.105; opentui'sv0.1.106andv0.2.0haven't shipped any CodeRenderable changes since the Explain usage of external references in AGENTS.md #965 fix).experimental.markdown = trueis set, or you're on the fallbackcodefiletype path.If the cut-off line consistently ends at the same kind of token (e.g. always inside
</<=/ a code-fence open), that points the second missing render trigger at the same lexer-state branch. Different tokens each time suggests a timing-only race.mrrewilh commentedon Apr 28, 2026
❯ opencode --version
1.14.29
Model: MiniMax M2.5 Free
experimental.markdown: default one i didnt edit any json or config.
Cut-off output:
Let me create a comprehensive plan. // This is the thinking part (the gray area)
Based on research, here's my plan:
Plan: Core Search, Filter & VRDB Integration
Field
name, package_name
version, size_mb, downloads
rating, rating_count
Missing from VR Src: developer, genres, description, images, languages, comfort level
core/src/catalog/
├── loader.rs # existing - load catalog
├── search.rs # NEW - search/filter functions
└── mod.rs
Functions:
▣ Plan · MiniMax M2.5 Free · 35.4s
Copied from the TUI.
truffle-dev commentedon Apr 28, 2026
Thanks for the version and cut-off text. Two updates from my earlier note:
Correction:
OPENCODE_EXPERIMENTAL_MARKDOWNdefaults to true — the flag uses!falsy(...)inpackages/core/src/flag/flag.ts, so an unset env var resolves totrue. At default config you're rendering throughMarkdownRenderable, not the<code filetype="markdown">fallback. The fix in opentui#965 was onCodeRenderable.contentsetter, which is a different layer than the one your repro is exercising. That was my read miss.MarkdownRenderableparses incrementally and creates child renderables per block (oneCodeRenderableper markdown block, separate ones for fenced code). Withstreaming={true}it keeps the trailing 2 tokens "unstable" until more content arrives. TheTextPartin opencode hardcodesstreaming={true}and never flips it to false, so at end-of-stream the trailing unstable tokens stay unstable indefinitely.One focused diagnostic to split where the text went:
After a cut-off, does pressing any key or scrolling refresh the display to show the missing text?
set contentshort-circuited somewhere).If you can also paste the complete text after streaming finishes (let it sit a moment, then copy from the TUI scrollback), that distinguishes "final display cut off" from "final buffer cut off".
mrrewilh commentedon Apr 29, 2026
No, I'm one million percent sure i can't see missing text. For example;
▣ Plan · MiniMax M2.5 Free · 41.0s
This was copied from TUI, after i left session with
exitcommand then, continued withopencode -s ses_bla_blaAlso in case if it matters, first it stops for a second. Like something happened (?) then it ends.
@truffle-dev
8 remaining items
truffle-dev commentedon May 3, 2026
The SQL output settles where the content goes: storage row matches the TUI cut-off character-for-character, so the stream/storage write path isn't dropping chunks — opencode is persisting exactly what the model returned, and rendering exactly that.
What I think is happening on those plans: the cut-off site (
fn fetch_game(&self, game_id: &strmid-line, mid-token) is the shape you'd expect when the model hits its output-token limit. MiniMax M2.5 Free's per-request output cap is well under opencode's 32k default, so on a long plan the response truncates mid-content and the stream finishes withfinishReason: "length". opencode then writesmessage.finish = "length", the message is treated as final, and the TUI shows the truncated text without surfacing the cause. The "stops for a second, then ends" pause matches the provider closing the stream after the limit is reached, not a render race.There's nothing on the opencode side that can recover the missing tokens (the model never emitted them), but the cause should at least be visible. Opened #25557 to add a non-stop finish-reason indicator next to the existing
· interruptedsuffix in the assistant header — forlength/content-filter/erroryou'd see e.g.▣ Plan · MiniMax M2.5 · 41.0s · truncated by length limit. Same suffix is added to the markdown transcript export.Workaround on your side until/if MiniMax raises the free-tier cap: switch to a model with a higher output cap for long plans (Claude / Sonnet, Gemini 2.5 Pro, GPT-4o all have higher per-request output budgets than MiniMax M2.5 Free), or shorten the prompt so the response fits.
mrrewilh commentedon May 3, 2026
After the latest cut-off, i prompted minimax to continue.
As you can see, it cuts on short prompt and even short answer.
Query for this session;
truffle-dev commentedon May 3, 2026
The continue-attempt reproducing the cut-off (and storage matching the TUI again) is good signal. It rules out "first response was unlucky" and points at a stable mechanism. It also undermines my earlier "model hit length" framing: both the long initial response and the short continue response cut off, which is the wrong shape for a fixed output-token budget.
The next diagnostic lives one table up.
partonly stores the textual content; the finish reason and any error live onmessage.data. If you can run:Three branches from the result:
finish = "length": model self-reported hitting its output cap. PR fix(tui): surface non-stop finish reasons in assistant header #25557 surfaces this in the TUI as· truncated by length limit. Underlying ask is "raise the model's output limit" (for MiniMax via OpenRouter, that's the upstream provider'smax_tokensdefault).finish = "error"anderrorpopulated: stream errored mid-flight. PR fix(tui): surface non-stop finish reasons in assistant header #25557 surfaces this as· ended with error, and the partial content stays visible.finishis null/empty on the truncated rows: most likely the SSE stream was cut before anyfinish-stepevent arrived. opencode setsassistantMessage.finishonly when it receives a finish-step event from the model (packages/opencode/src/session/processor.ts:470); the only fallback to"stop"sits in the structured-output branch (prompt.ts:1591). A network-level disconnect on free-tier MiniMax leavesfinishundefined, the message renders without any suffix, and the truncation is invisible. PR fix(tui): surface non-stop finish reasons in assistant header #25557 does NOT yet cover this case.The "even short prompts and short answers" datapoint plus the consistent cut-offs really point at branch three. If your query confirms
finishis null on the truncated rows, I'll send a follow-up tracking whether a finish-step ever arrived and surfacing· stream cutin the TUI for that case.mrrewilh commentedon May 3, 2026
query-1.json
truffle-dev commentedon May 3, 2026
Thanks for the query. The data rules out all three branches I pointed at: no
length, noerror, no nullfinish. Two rows endedstop(742 and 1194 output tokens), three endedtool-calls(65, 187, 331). The model is self-reporting normal completion every time. PR #25557 only attaches a suffix when finish islength,error, orcontent-filter, so it won't surface anything for these rows. The follow-up patch I floated for "stream cut before finish-step" doesn't apply either, because the finish event clearly arrived.That changes the question. Two failure modes still fit the "feels cut off" symptom:
Model-quality terseness on the
stoprows. 742 visible tokens is roughly 500-800 words, substantive, but free-tier MiniMax sometimes wraps a complex ask in a short summary instead of working through it. If the truncated message you screenshotted earlier is one of thesestoprows, this is upstream behavior, not opencode dropping content. The diagnostic: pick the row withfinish="stop"andoutput_tokens=742, runIf the
textpart holds a substantial response (a few thousand chars), the model wrote what it intended and the cut-off is perceptual. If thetextpart is much shorter than 742 tokens would predict, something is dropping content between the SSE stream and the part write. That's a real opencode bug worth tracking separately.Tool-loop interruption on the
tool-callsrows. Eachfinish="tool-calls"row should be followed by tool execution and another assistant step. If the next step never fires (free-tier rate-limit between turns, OpenRouter reconnect, etc.), the conversation stalls on a short message and looks cut off. To check, take the most recentfinish="tool-calls"row'sidand runIf there's no follow-up assistant or tool message, the tool loop bailed silently after the model emitted calls. That's a different fix path (heartbeat or retry on tool-loop continuation), not a finish-suffix.
Either result narrows the next move. Apologies for the wrong branches in the first reply. Your continue-attempt evidence pushed me one table up, and the data pushed me one layer further than that.
mrrewilh commentedon May 3, 2026
I reverted those messages. It still happens but i was trying to continue. Its not possible tho. Sorry for this, cant go back to them right now.
query-2.json
The fixed queries (as .json) for my current session (?);
For tool-loop
Also a brief query about all messages
query-7.json
truffle-dev commentedon May 3, 2026
Two things land from the new queries:
Hypothesis (a) confirmed. The
stoprow reports 172 output tokens. English averages roughly 4 chars per token, so 172 tokens predicts something around 600 to 800 chars. The storedtextpart is 296 chars and ends in the middle of a word (Fixed Telegram HTML parsing (uses). That mid-word terminator is the smoking gun. If the model had naturally stopped there, the last token would be a punctuation or sentence end, not an open paren followed by a verb.Hypothesis (b) ruled out. Every assistant row in
query-7.jsonhas a populatedfinish(onestopplus ninetool-calls). The tool loop is advancing normally, no silent bail.So the question is where the drop happens between the model emitting and the part being saved. opencode's text accumulator is at
packages/opencode/src/session/processor.ts:533-544:It just concatenates whatever arrives, no filtering. So the deltas opencode sees are the deltas the AI SDK demuxed from the SSE stream, which are the deltas the OpenRouter free-tier proxy forwarded from MiniMax. The mid-word cut plus the
finish="stop"event arriving cleanly afterward fits one shape best: the upstream proxy truncated the model's actual output and emitted a syntheticfinish_reason="stop"event. From opencode's perspective the stream looked complete, so it stored what it got and rendered no truncation suffix.The original
</<=correlation may be a related but distinct symptom of the same upstream behavior. The current session's truncated text contains no<, and it still cuts mid-word. Whatever the proxy is doing, it's not strictly conditional on those characters.Two opencode-side fixes worth considering separately from PR #25557:
output_tokensexceedslength(text part) / 3by some margin (say 2x), surface· response may be truncated upstreamin the assistant header. Catches synthesized-stop cases that PR fix(tui): surface non-stop finish reasons in assistant header #25557'slength/error/content-filterswitch can't see.text-deltaevents received and the cumulative chars per part. If the count looks plausible (e.g. 50 deltas averaging ~6 chars) the truncation is upstream; if there's a sudden gap (say 50 deltas then nothing for 10s beforefinish-step), the truncation is between AI SDK and opencode.Happy to ship either if the maintainers want one. The first is a small TUI patch on top of #25557; the second is a few logs in
processor.tsbehind aDEBUG=opencode:streamflag.For your immediate situation: a paid-tier OpenRouter key or a different provider would test the upstream-truncation theory directly. If the same prompt completes cleanly on a non-free MiniMax key (or any other model), the proxy is the culprit and the fix path is upstream. If it still truncates everywhere, that's stronger signal for an opencode-side stream parsing issue and the audit log would catch it.
mrrewilh commentedon May 3, 2026
Thanks.
I thought this was about me but this wasn't a issue about < or <= operators at all for me.
So if @blankyblank is good to go, we can close the issue. If a cut-off happens again i can open a new issue.
truffle-dev commentedon May 3, 2026
Sounds good. To leave a useful breadcrumb for anyone who lands here later: the diagnostic narrowed to opencode receiving stream events that look complete (text-deltas + a clean
finish-stepwithreason="stop"), but the stored text is shorter thanoutput_tokenspredicts and ends mid-word. That fits an upstream proxy synthesizing a stop event after truncating the model's actual output, with opencode faithfully recording what arrived.Two opencode-side fixes that would surface this to users (separate from #25557, which only catches
length/error/content-filter) if maintainers want them as actionable items:output_tokensexceedslength(text) / 3by a margin (catches synthesized-stop cases).DEBUG=opencode:streamlog of text-delta event count and cumulative chars per part (lets a user distinguish upstream truncation from any AI-SDK-to-opencode delta drop).Happy to ship either as a follow-up PR if there's interest. Otherwise leaving the diagnostic here so the next person hitting this can skip the four rounds.
github-actions commentedon Jul 4, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.
blankyblank commentedon Jul 5, 2026
sorry just saw this.
i haven't been noticing it lately. so it seems to have been resolved at some point. Not sure if it was fixed by some unrelated change, or something earlier after I reported the issue.
while the issue was happening, it definitely wasn't getting cut off because of length. it would get caught off sometimes on the first sentence if i couldn't get the model to start again on the line after the > (or it might have been < ) occured.
But like I said, it seems to have been resolved as of now.
3xecutablefile commentedon Aug 22, 2026
I have noticed this with the new 0x alpha model, which cuts off every 5 minutes or so