Skip to content

[4 BUGS ] Found around TIME SPACE, NUMERIC ERROR CODE , INCOMPLETNESS OF ERROR HANDLING & CONTEXT CACHING !!! #33380

Description

@AdityaPagare619

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.ts

The Code (Lines ~480-500)

case "reasoning-delta":
  // Match dev: silently drop orphan deltas (no preceding reasoning-start).
  if (!(value.id in ctx.reasoningMap)) return
  ctx.reasoningMap[value.id].text += value.text  // ← O(N²) BUG
  // ...

case "text-delta":
  if (!ctx.currentText) return
  ctx.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²)

Empirical evidence from GitHub Issue #30067:

Run config Per-step avg 2h timeouts
Unpatched 61s 28% (5/18)
Patched 21s 0% (0/53)

Impact on my working cli session:

  • The 100-step sequential thinking generated ~50+ reasoning deltas
  • Each delta triggered V8 rope flattening via the bus event broadcaster
  • Per-step latency grew: 6s → 30s → 60s → 100s+
  • Session effectively hung after ~50 turns

Bug 2: Numeric Error Code Classification Failure

File: packages/opencode/src/session/retry.ts

The Code (Lines ~62-70)

const json = parseJSON(msg)
if (!json || typeof json !== "object") return undefined
const code = typeof json.code === "string" ? json.code : ""  // ← BUG

if (json.type === "error" && json.error?.type === "too_many_requests") {
  return { message: "Too Many Requests" }
}
if (code.includes("exhausted") || code.includes("unavailable")) {  // ← NEVER MATCHES
  return { message: "Provider is overloaded" }
}

Mathematical Proof

Given:

  • OpenRouter returns: {"code":502,"message":"Network connection lost.","metadata":{"error_type":"provider_unavailable"}}
  • json.code = 502 (type: number)

Type check:

typeof json.code === "string"
= typeof 502 === "string"
= "number" === "string"
= false

Result:

code = ""  (empty string)

String matching:

"".includes("exhausted")   = false
"".includes("unavailable") = false

Conclusion: The retry check NEVER fires for numeric error codes.

Additional missing check: json.metadata.error_type is never inspected.

Evidence from GitHub Issue #22448:

"From my session database, I've logged 19 instances of 502 Network connection lost errors across April 9-14. None were retried."


Bug 3: Incomplete Error Classification in fromError()

File: packages/opencode/src/session/message-v2.ts

The Code (Lines ~340-400)

export function fromError(
  e: unknown,
  ctx: { providerID: ProviderV2.ID; aborted?: boolean },
): NonNullable<Assistant["error"]> {
  switch (true) {
    case e instanceof DOMException && e.name === "AbortError":
      return new AbortedError(/*...*/).toObject()
    // ...
    case (e as SystemError)?.code === "ECONNRESET":
      return new APIError({ isRetryable: true, /*...*/ }).toObject()
    // ...
    case e instanceof Error:
      return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject()
    // ...
  }
}

Mathematical Proof

Error classification coverage:

Let E = set of all possible network errors
Let R = set of errors classified as retryable by fromError()

Currently in R:

R = {ECONNRESET, ZlibError, HeaderTimeoutError, ResponseStreamError, APICallError}

Missing from R (transient network errors):

M = {ETIMEDOUT, ENOTFOUND, EAI_AGAIN, ECONNREFUSED, EHOSTUNREACH,
     ENETUNREACH, EPIPE, "fetch failed", "socket hang up",
     "terminated", "other side closed"}

Coverage ratio:

|R| / (|R| + |M|) = 5 / (5 + 11) = 31.25%

68.75% of transient network errors are NOT classified as retryable.

Impact chain:

1. Network transient error occurs (e.g., undici "terminated")
2. fromError() classifies as NamedError.Unknown (not retryable)
3. retryable() returns undefined (no retry)
4. Session processor calls halt() → session dies
5. User sees "Network connection lost" with no recovery

Bug 4: Context Inflation via cache.read (Issue #30649)

File: packages/opencode/src/session/session.ts (estimated)

The Code Pattern

// Session-level token counters grow unbounded
session.tokens_input += usage.input
session.tokens_output += usage.output
session.tokens_cache_read += usage.cache.read  // ← ACCUMULATES FOREVER

Mathematical Proof

Given:

  • A session with n turns
  • 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.

Evidence from GitHub Issue #30649:

"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 end
if (!ctx.currentText._chunks) ctx.currentText._chunks = []
ctx.currentText._chunks.push(value.text)
// In text-end handler:
ctx.currentText.text = ctx.currentText._chunks.join("")

Complexity: O(n) total instead of O(n²)

Fix 2: Numeric Error Code Handling

Current:

const code = typeof json.code === "string" ? json.code : ""

Fix:

const code = typeof json.code === "string"
  ? json.code
  : typeof json.code === "number"
    ? String(json.code)
    : ""
const errorType = typeof json.metadata?.error_type === "string"
  ? json.metadata.error_type
  : ""

if (code.includes("exhausted") || code.includes("unavailable") || errorType.includes("unavailable")) {
  return { message: "Provider is overloaded" }
}
if (["502", "503", "524"].includes(code)) {
  return { message: "Provider temporarily unavailable" }
}

Fix 3: Expand Error Classification

Current:

case (e as SystemError)?.code === "ECONNRESET":
  return new APIError({ isRetryable: true, /*...*/ }).toObject()

Fix:

case ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN",
      "ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH", "EPIPE"]
      .includes((e as SystemError)?.code ?? ""):
  return new APIError({ isRetryable: true, /*...*/ }).toObject()
case e instanceof Error && /terminated|fetch failed|socket hang up|other side closed/i.test(e.message):
  return new APIError({ isRetryable: true, /*...*/ }).toObject()

Fix 4: Separate Billing from Context Accounting

Current:

session.tokens_cache_read += usage.cache.read

Fix:

// Keep billing counters separate from context accounting
session.billing_tokens_cache_read += usage.cache.read
// For context/compaction decisions, use only current message sizes
session.effective_context_tokens = calculateCurrentContext(messages)

Version Matrix

Bug Affects Versions Fix PR Status
O(N²) text accumulation All versions #30067 Open
Numeric error codes All versions #22448 Open
Incomplete error classification All versions #30638 PR open
Context inflation via cache.read v1.14.24+ #30649 Open

References

  1. GitHub Issue 502 provider_unavailable errors from OpenRouter are not retried, causing subagent/session aborts #22448 - 502 provider_unavailable errors not retried
  2. GitHub Issue [BUG]: Desktop task execution is unexpectedly interrupted with file watcher Invalid handle and undici terminated errors on Windows #29589 - Desktop task interrupted with undici terminated errors
  3. GitHub Issue session/processor: text/reasoning delta accumulation is O(N²); long agent loops hang at 50-80 turns #30067 - O(N²) text/reasoning delta accumulation
  4. GitHub Issue Session token usage grows unbounded via cache.read and causes unrecoverable context-window errors #30649 - Session token usage grows unbounded via cache.read
  5. GitHub Issue [BUG] OpenCode crashes on long sessions. #22883 - OpenCode crashes on long sessions (OOM)
  6. GitHub PR fix(session): retry network errors, cap at 3, add retry_exhausted status #28792 - fix(session): retry network errors, cap at 3
  7. GitHub PR fix(session): classify transport and timeout errors as retryable #30638 - Widen retryable error classification
  8. GitHub Issue Network Connection Lost #20060 - Network Connection Lost (labeled bug + core)

Plugins

yggdrasil

OpenCode version

1.17.9

Steps to reproduce

The steps as per BUGS,
BUG 1 :
Steps to reproduce:

  1. Start a session with a thinking-mode model (DeepSeek V4, Kimi K2, Qwen3-thinking, or similar)

  2. Use sequential thinking or a task that generates many small reasoning deltas
    (e.g., 100-step sequential thinking review)

  3. 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
  4. 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 :

  1. Configure OpenCode with OpenRouter as provider

  2. Start a long-running session or subagent

  3. Wait for OpenRouter to return a 502 error:
    {"code":502,"message":"Network connection lost.","metadata":{"error_type":"provider_unavailable"}}

  4. Check the logs — the error shows as "Network connection lost" with no retry

  5. 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 "".

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:

  1. Use a model with a defined context limit (e.g., mimo-v2.5-free @ 1,048,576 tokens)

  2. Run a long session until you're at the limit:
    Check: SELECT tokens_input + tokens_cache_read FROM session WHERE id = 'YOUR_SESSION'

  3. Send one more message that would exceed the limit

  4. Session dies with "Network connection lost" — no compaction, no retry

  5. 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 :

  1. Start a long-running session with a provider that reports cache read tokens

  2. Keep the session active for many turns (50+ turns with cache hits)

  3. Check the session database:
    SELECT tokens_cache_read, tokens_input, tokens_output FROM session WHERE id = 'YOUR_SESSION'

  4. You'll see something like:
    tokens_input: 14,774,671
    tokens_output: 77,881
    tokens_cache_read: 41,255,296 ← cumulative, not current context

  5. The session thinks it has 56M tokens of context when it actually has ~2MB of data

  6. 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

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


Screenshot and/or share link

No response

Operating System

Windows 11

Terminal

Windows Terminal

Metadata

Metadata

Assignees

Labels

needs:complianceThis means the issue will auto-close after 2 hours.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions