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
I was working on a project it went intentionally massive with spending over 40M Input tokens but all of sudden it crashed while doing forensics of evidences figured out these 3 BUGS which are as following :
Bug 1: O(N²) Text Delta Accumulation in Processor
File: packages/opencode/src/session/processor.ts
The Code (Lines ~480-500)
case"reasoning-delta":
// Match dev: silently drop orphan deltas (no preceding reasoning-start).if(!(value.idinctx.reasoningMap))returnctx.reasoningMap[value.id].text+=value.text// ← O(N²) BUG// ...case"text-delta":
if(!ctx.currentText)returnctx.currentText.text+=value.text// ← O(N²) BUG// ...
Mathematical Proof
Given:
Let n = total number of delta events in a session
Let k_i = length of the i-th delta string (in characters)
Let S_i = Σ(k_j for j=1..i) = cumulative length after i deltas
Cost of each += operation:
In JavaScript/V8, str += chunk on a rope/cons-string triggers flattening when the string is accessed externally (e.g., by the UI render loop, NDJSON serializer, or bus event broadcaster).
Flattening cost: O(S_i) — must copy all S_i characters to a new contiguous buffer.
Total cost across all n deltas:
T(n) = Σ(S_i for i=1..n)
= Σ(Σ(k_j for j=1..i) for i=1..n)
= Σ(k_j * (n - j + 1) for j=1..n) [by rearranging the double sum]
Worst case (all deltas equal length k):
T(n) = k * Σ(n - j + 1 for j=1..n)
= k * Σ(i for i=1..n)
= k * n(n+1)/2
= O(kn²)
Each turn sends C cache read tokens (the full context)
Cache read tokens are cumulative, not representative of current context
After n turns:
Total cache read = n * C
Your session:
C ≈ 39M tokens (actual context)
n ≈ 2028 messages / 2 messages per turn ≈ 1014 turns
Total cache read ≈ 937M tokens (937,559,056)
The problem: If any compaction/routing logic uses tokens_cache_read as a proxy for current context size, it will think the context is 937M tokens when it's actually ~39M.
"The inflated value is dominated by cached reads rather than actual message text."
"session-level cumulative tokens_cache_read can dominate token totals and make context/compaction behavior diverge from real prompt size."
Combined Kill Chain (For Session)
1. Session accumulated 937M cache read tokens (Bug #4)
→ OpenCode may have treated this as actual context size
2. 100-step sequential thinking hit O(N²) slowdown (Bug #1)
→ Per-step latency grew from 6s to 100s+
→ Session effectively hung
3. yggdrasil tool had JSON parsing issues with long strings
→ Tool calls failed with "invalid" errors
4. Request payload became too large for provider
→ Provider returned transient error
5. Error classification missed the error type (Bug #3)
→ Classified as NamedError.Unknown (not retryable)
6. Retry logic failed for numeric error codes (Bug #2)
→ Even if classified, code check would fail
7. Session died with "Network connection lost"
→ No recovery attempted
Proposed Fixes
Fix 1: O(N²) → O(N) Text Accumulation
Current:
ctx.currentText.text+=value.text// O(n²) total
Fix:
// Use array accumulator, join once at endif(!ctx.currentText._chunks)ctx.currentText._chunks=[]ctx.currentText._chunks.push(value.text)// In text-end handler:ctx.currentText.text=ctx.currentText._chunks.join("")
// Keep billing counters separate from context accountingsession.billing_tokens_cache_read+=usage.cache.read// For context/compaction decisions, use only current message sizessession.effective_context_tokens=calculateCurrentContext(messages)
The steps as per BUGS, BUG 1 :
Steps to reproduce:
Start a session with a thinking-mode model (DeepSeek V4, Kimi K2, Qwen3-thinking, or similar)
Use sequential thinking or a task that generates many small reasoning deltas
(e.g., 100-step sequential thinking review)
Monitor per-step latency as the session grows:
Turn 1-10: ~6s per step
Turn 20-30: ~30s per step
Turn 40-50: ~60s per step
Turn 50+: hangs or exceeds 2-hour timeout
Check the logs — you'll see V8 doing string rope flattening:
each "reasoning-delta" or "text-delta" event triggers
ctx.currentText.text += value.text which copies the entire
accumulated string on every access by the UI render loop
Where the bug is:
packages/opencode/src/session/processor.ts
Lines handling "reasoning-delta" and "text-delta" events:
case "reasoning-delta":
ctx.reasoningMap[value.id].text += value.text // O(N²)
case "text-delta":
ctx.currentText.text += value.text // O(N²)
Expected: Per-step latency stays roughly constant (~6s)
Actual: Latency grows quadratically, eventually hanging
Bug 2 :
Configure OpenCode with OpenRouter as provider
Start a long-running session or subagent
Wait for OpenRouter to return a 502 error:
{"code":502,"message":"Network connection lost.","metadata":{"error_type":"provider_unavailable"}}
Check the logs — the error shows as "Network connection lost" with no retry
Check the session database:
SELECT data FROM message WHERE data LIKE '%Network connection lost%'
— you'll find multiple unretried instances
Where the bug is:
packages/opencode/src/session/retry.ts
_> When json.code is 502 (a number), this becomes "".
The check code.includes("unavailable") never matches.
Expected: 502 errors trigger retry with backoff
Actual: 502 errors kill the session immediately_
Bug 3 :
Steps to reproduce:
Use a model with a defined context limit (e.g., mimo-v2.5-free @ 1,048,576 tokens)
Run a long session until you're at the limit:
Check: SELECT tokens_input + tokens_cache_read FROM session WHERE id = 'YOUR_SESSION'
Send one more message that would exceed the limit
Session dies with "Network connection lost" — no compaction, no retry
Check session data:
The error shows as:
{"name":"UnknownError","data":{"message":""Network connection lost.""}}
Where the bug is:
packages/opencode/src/session/message-v2.ts
Line ~716:
case e instanceof Error:
return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject()
_> Context overflow errors wrapped in generic Error fall through here.
Expected: Context overflow triggers auto-compaction
Actual: Session dies immediately_
Bug 4 :
Start a long-running session with a provider that reports cache read tokens
Keep the session active for many turns (50+ turns with cache hits)
Check the session database:
SELECT tokens_cache_read, tokens_input, tokens_output FROM session WHERE id = 'YOUR_SESSION'
You'll see something like:
tokens_input: 14,774,671
tokens_output: 77,881
tokens_cache_read: 41,255,296 ← cumulative, not current context
The session thinks it has 56M tokens of context when it actually has ~2MB of data
Auto-compaction triggers based on the inflated count, or the provider
rejects the request because it thinks the context is too large
Where the bug is:
packages/core/src/session/projector.ts
This accumulates forever. If any compaction logic uses this as current
context size, it will trigger premature overflow.
Expected: Context checks use actual message size, not cumulative counters
Actual: Cumulative cache.read inflates perceived context size
Description
I was working on a project it went intentionally massive with spending over 40M Input tokens but all of sudden it crashed while doing forensics of evidences figured out these 3 BUGS which are as following :
Bug 1: O(N²) Text Delta Accumulation in Processor
File:
packages/opencode/src/session/processor.tsThe Code (Lines ~480-500)
Mathematical Proof
Given:
n= total number of delta events in a sessionk_i= length of the i-th delta string (in characters)S_i = Σ(k_j for j=1..i)= cumulative length after i deltasCost of each
+=operation:In JavaScript/V8,
str += chunkon a rope/cons-string triggers flattening when the string is accessed externally (e.g., by the UI render loop, NDJSON serializer, or bus event broadcaster).Flattening cost: O(S_i) — must copy all S_i characters to a new contiguous buffer.
Total cost across all n deltas:
Worst case (all deltas equal length k):
Empirical evidence from GitHub Issue #30067:
Impact on my working cli session:
Bug 2: Numeric Error Code Classification Failure
File:
packages/opencode/src/session/retry.tsThe Code (Lines ~62-70)
Mathematical Proof
Given:
{"code":502,"message":"Network connection lost.","metadata":{"error_type":"provider_unavailable"}}json.code = 502(type:number)Type check:
Result:
String matching:
Conclusion: The retry check NEVER fires for numeric error codes.
Additional missing check:
json.metadata.error_typeis never inspected.Evidence from GitHub Issue #22448:
Bug 3: Incomplete Error Classification in fromError()
File:
packages/opencode/src/session/message-v2.tsThe Code (Lines ~340-400)
Mathematical Proof
Error classification coverage:
Let
E= set of all possible network errorsLet
R= set of errors classified as retryable byfromError()Currently in R:
Missing from R (transient network errors):
Coverage ratio:
68.75% of transient network errors are NOT classified as retryable.
Impact chain:
Bug 4: Context Inflation via cache.read (Issue #30649)
File:
packages/opencode/src/session/session.ts(estimated)The Code Pattern
Mathematical Proof
Given:
nturnsCcache read tokens (the full context)After n turns:
Your session:
The problem: If any compaction/routing logic uses
tokens_cache_readas a proxy for current context size, it will think the context is 937M tokens when it's actually ~39M.Evidence from GitHub Issue #30649:
Combined Kill Chain (For Session)
Proposed Fixes
Fix 1: O(N²) → O(N) Text Accumulation
Current:
Fix:
Complexity: O(n) total instead of O(n²)
Fix 2: Numeric Error Code Handling
Current:
Fix:
Fix 3: Expand Error Classification
Current:
Fix:
Fix 4: Separate Billing from Context Accounting
Current:
Fix:
Version Matrix
References
provider_unavailableerrors from OpenRouter are not retried, causing subagent/session aborts #22448 - 502 provider_unavailable errors not retriedPlugins
yggdrasil
OpenCode version
1.17.9
Steps to reproduce
The steps as per BUGS,
BUG 1 :
Steps to reproduce:
Start a session with a thinking-mode model (DeepSeek V4, Kimi K2, Qwen3-thinking, or similar)
Use sequential thinking or a task that generates many small reasoning deltas
(e.g., 100-step sequential thinking review)
Monitor per-step latency as the session grows:
Check the logs — you'll see V8 doing string rope flattening:
each "reasoning-delta" or "text-delta" event triggers
ctx.currentText.text += value.text which copies the entire
accumulated string on every access by the UI render loop
Bug 2 :
Configure OpenCode with OpenRouter as provider
Start a long-running session or subagent
Wait for OpenRouter to return a 502 error:
{"code":502,"message":"Network connection lost.","metadata":{"error_type":"provider_unavailable"}}
Check the logs — the error shows as "Network connection lost" with no retry
Check the session database:
SELECT data FROM message WHERE data LIKE '%Network connection lost%'
— you'll find multiple unretried instances
Where the bug is:
packages/opencode/src/session/retry.ts
Line ~62:
const code = typeof json.code === "string" ? json.code : ""
_> When json.code is 502 (a number), this becomes "".
Bug 3 :
Steps to reproduce:
Use a model with a defined context limit (e.g., mimo-v2.5-free @ 1,048,576 tokens)
Run a long session until you're at the limit:
Check: SELECT tokens_input + tokens_cache_read FROM session WHERE id = 'YOUR_SESSION'
Send one more message that would exceed the limit
Session dies with "Network connection lost" — no compaction, no retry
Check session data:
The error shows as:
{"name":"UnknownError","data":{"message":""Network connection lost.""}}
Where the bug is:
packages/opencode/src/session/message-v2.ts
Line ~716:
case e instanceof Error:
return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject()
_> Context overflow errors wrapped in generic Error fall through here.
Bug 4 :
Start a long-running session with a provider that reports cache read tokens
Keep the session active for many turns (50+ turns with cache hits)
Check the session database:
SELECT tokens_cache_read, tokens_input, tokens_output FROM session WHERE id = 'YOUR_SESSION'
You'll see something like:
tokens_input: 14,774,671
tokens_output: 77,881
tokens_cache_read: 41,255,296 ← cumulative, not current context
The session thinks it has 56M tokens of context when it actually has ~2MB of data
Auto-compaction triggers based on the inflated count, or the provider
rejects the request because it thinks the context is too large
Where the bug is:
packages/core/src/session/projector.ts
tokens_cache_read: SessionTable.tokens_cache_read + value.tokens.cache.read
Screenshot and/or share link
No response
Operating System
Windows 11
Terminal
Windows Terminal