Repository navigation
Client stalls at single-core 100% CPU during long sessions with many rapid tool-calling turns — unrecoverable except via SIGINT #42567
Description
Title: Client stalls at single-core 100% CPU during long sessions with many rapid tool-calling turns — unrecoverable except via SIGINT
Summary
During a long-running session involving a background job monitored via repeated short tool-calling turns, the OpenCode client stalls completely — no partial output, no tool call, nothing — for 20+ minutes. Escape does not cancel the stalled turn. Only killing the process from the terminal (Ctrl+C) recovers, requiring the session to be resumed afterward. Confirmed reproducible twice in the same day, same workflow signature.
Environment
- Provider:
github-copilot, model:claude-sonnet-5 - Platform: Linux (see full env details on request)
- Session was long-running (~1.5h+) at the time of the stall, with substantial accumulated context (
cache.read~248,000 tokens by the time of the incident)
Reproduction pattern
- Start a long-running background job (in this case via
tmux new -d -s NAME '<command> | tee log'). - Poll it repeatedly from the agent in short tool-calling turns (in this case: ~12-13 turns of
sleep 115; tmux list-sessions; cat log, each turn ~2 minutes apart). - After the background job actually completes (~20-25 min in), the next agent turn — which should be cheap (just reading a finished log / detecting the tmux session ended) — instead stalls completely for 20+ minutes before producing any output at all.
Evidence gathered (from ~/.local/share/opencode/opencode.db)
Querying the local SQLite session DB (message, part, event tables) for the stalled turn showed:
- The stalled message's own
time.createdtimestamp was ~23.5 minutes before its own firstpart(not even astep-startpart) appeared. This rules out "a tool/bash call took a long time to execute" — the stall happened before the turn even began producing anything. - The eventual token/cost breakdown for that same message (once it finally completed) was:
input: 2, output: 426, reasoning: 0, cache.read: 247987, cache.write: 597,finish: "tool-calls". This is a cheap, almost entirely cache-hit request — model inference for 426 output tokens should take seconds, not 23+ minutes. - This rules out a slow/expensive model generation as the cause of the wall-clock delay.
Key additional fact: CPU was pinned at 100% on a SINGLE core during the stall (both occurrences)
This is the detail that rules out "waiting on a slow/stalled network response" (which would show near-zero CPU, blocked on I/O) and instead points to a synchronous, non-yielding operation blocking the client's single-threaded main/event-loop. Single-core saturation + a completely unresponsive UI (Escape doesn't register at all) is consistent with the JS/Bun main thread being busy in a tight loop or an O(n²)-or-worse computation, not legitimate parallel work and not I/O wait.
Escape failing to cancel is consistent with this: if the main event loop is blocked synchronously, it cannot process the keypress event that would trigger a cancel action. Ctrl+C works because the terminal's line discipline delivers SIGINT to the process at the OS level, independent of whether the app's own event loop is currently responsive — the process then dies (unhandled/stuck signal → default termination), which matches needing to "resume the session" afterward rather than a graceful cancel.
Hypothesis (unconfirmed — I don't have access to the OpenCode source to verify)
Given the correlation with (a) a large accumulated context (cache.read ~248k tokens) and (b) many rapid successive short tool-calling turns in the same session, I suspect some client-side bookkeeping step that runs synchronously on the main thread and scales with conversation/context size (e.g. serialization, token/cache accounting, or persistence to the local SQLite DB) has worse-than-linear complexity, and becomes a multi-minute main-thread stall once a session crosses some size/turn-count threshold. I have not been able to locate the specific code path causing this.
Request
- Any pointers to where in the codebase a per-turn synchronous operation might scale with session size would help narrow this down further.
- In the meantime, is there a supported way to reliably interrupt a stalled turn without killing the whole client process? (
Escapedid not work in either occurrence.)
Diagnostic method for anyone else hitting this
Query ~/.local/share/opencode/opencode.db directly:
-- find message.time_created (turn issued) vs its parts' time_created (turn actually starts producing output)
SELECT m.id, m.time_created,
(SELECT MIN(p.time_created) FROM part p WHERE p.message_id = m.id) as min_part,
(SELECT MAX(p.time_created) FROM part p WHERE p.message_id = m.id) as max_part
FROM message m WHERE m.session_id = '<session_id>' ORDER BY m.time_created;
-- pull full token/cost breakdown for a specific stalled message from the event log
SELECT data FROM event WHERE aggregate_id = '<session_id>' AND type = 'message.updated.1'
AND data LIKE '%<message_id>%' ORDER BY seq DESC LIMIT 1;Happy to provide the exact (anonymized) query outputs from both occurrences if useful.
Activity
shmty commentedon Aug 14, 2026
Closing — not pursuing this upstream for now.