Repository navigation
Description
Environment
- FreeBSD 15.1-RELEASE amd64
- opencode v2.0.27, built from the tag with a native FreeBSD Bun (bun 1.4.3,
bun-freebsd-x64) - Also reproduces in dev mode (
bun run src/index.ts serve), so this is not abun build --compileartifact
Symptom
opencode serve (and the TUI, same code path) starts fine and answers requests, then ~60-120s later the whole process wedges: new connections are accepted at TCP level but never get a response; every thread is parked in uwait/usem. SIGTERM during a service stop never completes either - the process has to be kill -9'd. Compiled v2.0.27 binary, dev-mode source, and serve --service all wedge identically.
ktrace evidence
Immediately before the event loop stops polling kqueue there is a thr_kill(<lwp>, SIGUSR1), then PSIG SIGUSR1 caught ... code=SI_LWP handled by a non-JSC handler, after which the loop never calls kevent() again and all threads sit in UMTX_OP_WAIT_UINT_PRIVATE/UMTX_OP_SEM2_WAIT. A janitor thread keeps running (madvise churn), so the process is not fully dead - JSC's suspend/resume handshake is.
Root cause
JSC picks its thread suspend/resume signal as SIGPWR on Linux but SIGUSR1 on every other POSIX target, including FreeBSD (wtf/posix/ThreadingPOSIX.cpp). packages/cli/src/heap.ts (Heap.listen, started from the CLI's main init at packages/cli/src/index.ts:85) registers process.on("SIGUSR1", handler), which installs Bun's JS signal handler over JSC's. From then on, the first goroutine-level suspend (GC / VMTraps) signals into Bun's handler instead of JSC's; globalSemaphoreForSuspendResume is never posted and the suspender never resumes. This is exactly the mechanism described upstream:
- FreeBSD: process hangs on exit after a successful conversation (JSC thread-suspend signal sent but never handled) oven-sh/bun#44595 - "FreeBSD: process hangs ... JSC thread-suspend signal sent but never handled"
- [jsc] don't override JSC's thread suspend signal on FreeBSD oven-sh/bun#44596 - "[jsc] don't override JSC's thread suspend signal on FreeBSD" (the bun-side fix; still open at the time of writing)
v1.x does not have heap.ts, which matches the observation that v1.19.0 on the same Bun/FreeBSD runtime is long-lived while v2.0.27 wedges.
Repro
- Native FreeBSD Bun 1.4.x, opencode v2 tag.
opencode serve &; one request (curl /api/info); wait ~2 minutes idle.- Process never answers again. With a
process.on("SIGUSR1", () => {})patch removed fromheap.ts, the same build stays alive (verified >4 minutes of repeated probing).
Suggested fix
Skip the SIGUSR1 listener on FreeBSD until bun lands #44596, e.g.:
if (process.platform === "win32" || process.platform === "freebsd") returnThe only lost feature is heap-snapshot-on-signal on FreeBSD. (CpuProfile.listen's SIGPROF listener is fine - SIGPROF does not collide with JSC.)
Notes
- A native FreeBSD build of opencode is not (yet) an upstream-supported target; happy to help test any fix. The only other FreeBSD-specific patches we carry are an
@opentui/coreplatform build and abuild.tstarget entry. - Probably also worth a note in FreeBSD: process hangs on exit after a successful conversation (JSC thread-suspend signal sent but never handled) oven-sh/bun#44595 that opencode v2 hits this out of the box on FreeBSD.
Activity
opencode-agent commentedon Oct 12, 2026
Thanks for the detailed report and the ktrace evidence.
I couldn't reproduce this here, because the sandbox only runs Linux. On Linux, JSC uses
SIGPWRto suspend and resume threads, so theSIGUSR1listener doesn't cause the hang there. This needs a maintainer with a FreeBSD machine and a native Bun build.I did check the code: on the latest
dev(80fb31e),Heap.listeninpackages/cli/src/heap.tsstill skips theSIGUSR1listener only onwin32. It still runs at CLI startup, which fits your explanation.Related issues:
I've left this for the maintainers to review, including your suggested platform check.