Skip to content

opencode spins one CPU core at ~100% indefinitely after bootstrap (no logs, no I/O, ignores SIGTERM) #32965

Closed as not planned

Description

@daveBifo

Bug: opencode main thread spins one CPU core at ~100% indefinitely (often right after a model stream step) — no logs, no I/O

Summary

On a large multi-module project, the opencode process repeatedly enters a CPU-bound loop: the main thread pins a single core at ~98–100% for many minutes, producing no further log output and no disk/DB I/O while it spins. The onset is usually mid-session, immediately after a model stream step (it was also seen once right after bootstrap). The hung process sometimes does not respond to SIGTERM (requiring SIGKILL), though in other occurrences it did terminate on SIGTERM. This has recurred multiple times in the same project and looks like an infinite loop in opencode itself rather than resource exhaustion on the host.

Version & Environment

  • opencode: 1.17.8 (confirmed latest; opencode upgrade reports "already installed")
  • Install method: standalone curl installer (~/.opencode/bin/opencode)
  • OS: Ubuntu 24.04.4 LTS
  • Kernel: 6.8.0-124-generic
  • Arch: x86_64
  • CPU: 8 cores · RAM: 15 GiB
  • Provider/model: zai-coding-plan / glm-5.2
  • File watcher backend: inotify (per log)

Project characteristics (likely contributing factors)

  • Large multi-module Gradle/Kotlin monorepo
  • ~2.1 GB on disk, 73,672 files total, of which ~12,432 live in build/ directories
  • 11 × node_modules directories
  • build/, .gradle/, node_modules/ are all listed in .gitignore
  • ~/.local/share/opencode/opencode.db has grown to ~3.6 GB (despite log showing cleanup prune=7.days)

Observed behavior / evidence

From ~/.local/share/opencode/log/opencode.log for a single run (run=cc853d12):

07:42:57  message="creating instance" directory=/path/to/project
07:42:58  message=bootstrapping
07:42:58  message="all LSPs are disabled"
07:42:58  message="all formatters are disabled"
07:42:58  message="booting location services"
07:42:58  message="watcher backend" backend=inotify
07:42:59  message="project copy refresh done"
07:43:59  message=cleanup prune=7.days        <-- LAST log line for this run

After 07:43:59 the process emitted zero further log lines and performed no DB/disk writes (opencode.db mtime stayed at 07:42), yet it kept running at ~98% CPU for ~9 minutes until it was killed.

Process snapshot during the hang:

PID    USER     %CPU  %MEM  ELAPSED  STAT  CMD
21731  clausi   98.2  13.0  08:05    Rl+   opencode
  • State R (running), 24 threads, ~2 GB RSS
  • In this occurrence kill -TERM had no effect (process kept running at 99%) and only kill -KILL terminated it; in later occurrences the process did exit on SIGTERM — so SIGTERM handling is inconsistent during the hang
  • Host load and Docker containers were otherwise idle — this is a pure single-core busy loop, not host overload

The bootstrap-time silence above was the first observed incident. The later, profiled incidents (see below) started mid-session, right after a model stream step rather than at bootstrap.

Steps to reproduce (best understanding)

  1. Open opencode in a large multi-module Gradle project (tens of thousands of files, sizeable build/ output present).
  2. Use a session that runs Gradle test builds (e.g. ./gradlew :service:test --rerun-tasks), generating heavy file churn under build/.
  3. Start/restart opencode in that directory.
  4. Observe one core pinned at ~100% with the log going silent right after the cleanup prune=... bootstrap line.

Reproducibility note: It is intermittent. A fresh, clean bootstrap of opencode in the same project did not reproduce it (the process bootstrapped normally and dropped to ~2% CPU, state Sl). The hang appears correlated with a specific session state — the affected sessions had previously been running repeated Gradle test loops, and/or were resuming a large existing session.

Captured native stack profile (root cause: main-thread tight loop)

I have now captured native stack profiles (gdb thread apply all bt, 3 samples ~1s apart) of two independent hung instances. Both point to the same thing:

  • The main thread ("opencode", the JS thread) is the only thread in state R and is spinning at a fixed instruction. In the cleanest capture, all 3 samples show the main thread frozen at the exact same PC:

    Thread 1 (LWP 74166 "opencode"):
    #0  0x0000000003b3faad in ?? ()
    #1  0x0000000000000000 in ?? ()
    
  • The earlier, separate hang (different PID, cwd = the project) hit the same address 0x0000000003b3faad in its first sample. → A deterministic busy loop at a fixed point in the opencode binary.

  • All HeapHelper (GC) threads are parked in futex/pthread_cond_timedwait — not GC.

  • No syscall/kernel frames on the spinning thread — pure userspace CPU loop, no I/O (consistent with the silent log and idle disk during the hang).

  • Onset: the hang begins immediately after a model stream step (message=stream providerID=... modelID=glm-5.2 ... agent=build), after which the log goes silent while CPU stays pinned.

The address has no symbol because the distributed binary is stripped (Bun single-file executable), so I can't resolve the JS/native frame name. A maintainer with symbols for the 1.17.8 release binary should be able to map 0x0000000003b3faad directly. Full profiles (all threads, per-thread CPU/wchan) available on request.

What would help confirm

I'm happy to provide the full raw profiles, or to re-capture with a JS-level profiler (e.g. Bun inspector / --inspect) if you can point me at the preferred way to attach to a live hung instance. A symbolicated build or a mapping for 0x0000000003b3faad in 1.17.8 would immediately localize the loop.

Workaround in place

A systemd watchdog now monitors per-process CPU and SIGKILLs any opencode process sustaining >90% of a core for >60s. This mitigates the symptom but not the root cause. The watchdog also auto-captures a gdb stack profile (all threads, 3 samples) of the hung process before killing it, so I can attach a real native stack trace from the next live occurrence.

Questions

  • Is there a recommended way to scope/exclude directories (build/, .gradle/, node_modules/) from the file watcher?
  • Is the ~3.6 GB opencode.db expected, or a sign of a pruning/vacuum issue that could also be contributing?

Activity

  1. github-actions commented on Jun 19, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your hang is related to the diff/patch calculation after a stream step, #29762 is likely the most relevant. If it occurs during reconnection or tool use, #15600 may apply.

  2. daveBifo commented on Jun 19, 2026

    @daveBifo
    Author

    As offered, here are the full raw stack profiles from the two captured runaways (sanitized; gdb thread apply all bt for all threads + per-thread CPU/wchan from /proc):

    Key excerpt (incident #2, identical across all 3 samples ~1s apart):

    Thread 1 (LWP 74166 "opencode"):   # main / JS thread, the only one in state R
    #0  0x0000000003b3faad in ?? ()
    #1  0x0000000000000000 in ?? ()
    

    All other threads (incl. HeapHelper GC threads) are parked in pthread_cond_timedwait/futex, and there are no syscall frames on the spinning thread — i.e. a pure userspace busy loop on the main thread, not GC and not I/O.

    These were captured by attaching gdb to the live hung process before killing it. Happy to re-capture with a JS-level profiler if you can suggest the preferred way to attach (e.g. Bun inspector) to localize the loop above the native frame.

  3. daveBifo commented on Jun 19, 2026

    @daveBifo
    Author

    Follow-up on triage / the auto-suggested duplicates.

    This looks like the same bug as #29762 ("'step-end' diff calculation blocks event loop indefinitely with 100% cpu"). The captured native profiles here line up with that report point-for-point:

    #29762 was filed against 1.15.11; this reproduces on 1.17.8, so it's still present. The gdb stack here (main thread frozen at a fixed PC, captured via attaching to the live hung process) may be useful corroboration since #29762 mentions profiling is hard due to the blocked loop.

    For the record — #1617 (getpaseo/paseo) cross-links this issue but is a different root cause and does not apply here:

    • allow Plugin type to be synchronous #1617 is Paseo spawning opencode serve with cwd=/, causing fff_search to scan the entire filesystem. That signature is multi-threaded (Rayon, 300%+ CPU) and heavily I/O-bound (openat/stat/read).
    • This report is standalone opencode with a normal cwd (the project / $HOME, never /), and the profile is the opposite: a single-thread, zero-I/O busy loop. So the cwd/fff_search-scan cause cannot be the explanation here.

    Happy to have this closed as a duplicate of #29762 if maintainers agree.

  4. daveBifo commented on Jun 19, 2026

    @daveBifo
    Author

    Another captured occurrence — the spin starts right after a stream step inside a subagent, with no write/edit involved

    I caught a fresh, clean occurrence with a 1 Hz /proc memory sampler plus an auto-archived session log. This one is the single-core ~100% spin variant (not the multi-core/heavy-GC memory blow-up I mentioned separately), and it adds a useful data point: it is not triggered by a file diff.

    Process signature during the hang (1 s /proc sampling, native profile):

    • RSS stayed flat at ~824 MB for the entire hang — no growth, so this is not memory/GC pressure.
    • Exactly 1 thread in state R, 18 in S (plus parked Bun Pool / watcher / HeapHelper threads) — a single busy thread pinning one core.
    • 0 swap used; host completely healthy. A watchdog terminated it via SIGTERM after ~60 s (it did honor SIGTERM this time).

    Onset (from the session log): the spin begins immediately after an LLM stream step, and this time that step was inside an explore subagent (agent=explore mode=subagent) which had just been doing a rapid sequence of read-only grep / glob / read tool calls. No write / edit / apply_patch ran before the freeze. So for this occurrence the loop is clearly not in file-diff computation — it points at step-end / streamed-response processing on the main thread.

    Sanitized tail of the log right before it went silent (paths, package, identifiers and domain terms anonymized):

    ... message=stream providerID=... modelID=... session.id=... agent=explore mode=subagent     (step 8)
    ... message=evaluated permission=grep pattern=<redacted>
    ... message=evaluated permission=glob pattern=services/<svc>/src/main/kotlin/<pkg>/domain/*.kt
    ... message=evaluated permission=read pattern=services/<svc>/src/test/kotlin/<pkg>/SomeIT.kt
    ... message="touching file" file=/path/to/project/services/<svc>/src/test/kotlin/<pkg>/SomeIT.kt
    ... message=loop session.id=... step=9
    ... message=stream providerID=... modelID=... agent=explore mode=subagent
    ... message=evaluated permission=grep pattern=<redacted>
    ... message=evaluated permission=read pattern=services/<svc>/src/main/kotlin/<pkg>/domain/SomeEvents.kt
    ... message="touching file" file=/path/to/project/services/<svc>/build.gradle.kts
    ... message="touching file" file=/path/to/project/services/<svc>/src/main/kotlin/<pkg>/domain/SomeEvents.kt
    ... message=loop session.id=... step=10
    ... message=stream providerID=... modelID=... agent=explore mode=subagent   <-- LAST line, then total silence + 100% CPU on one core
    

    So both occurrences I have now profiled look like the same step-end spin reached via two different paths: previously after a main-agent stream step, here after a subagent stream step that followed read-only tool calls. The common factor is "right after a stream step" — not file writes, not large diffs, not memory growth.

    Environment unchanged (opencode 1.17.8, Bun, glm-5.2 via zai-coding-plan, Ubuntu 24.04, x86_64). I'm happy to share the full sanitized session log and the native per-thread profile on request.

  5. rekram1-node commented on Jun 23, 2026

    @rekram1-node
    Contributor

    Automated: closing question issues.

    GitHub Issues are for bug reports and feature requests only — questions / support are no longer tracked here.

    If this is a bug or feature request, please open a new issue with the appropriate template.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions