Repository navigation
Description
Summary
A model turn can die silently mid-stream. Nothing is logged, the process goes idle, and time.idle never appears — but the session stays busy forever. Every prompt sent afterwards is accepted (HTTP 200) and queued behind the dead turn, yet never processed. There is no way for a client to detect or abort it: on this build POST /api/session/<id>/abort and GET /api/session/<id>/status both return 404.
Recovery requires killing the sidecar and starting fresh sessions.
Environment
- Desktop app
2.1.4, using its SSH sidecar path:opencode serve --serviceon loopback49374 - Servers: Ubuntu 22.04 and 24.04 (two hosts), plus a macOS client
- Providers: several; the failure is not tied to one provider/model as far as I can tell
- No proxies, no VPN in the loop for the affected hops
Reproduction pattern
Not deterministic per request, but reliably reproducible under a burst:
- Dispatch 10 independent conversations with concurrency 3 on one host → 4 of 10 sessions wedged
- The same kind of work at concurrency ≤2 on a second host stayed clean in the same period
- Resumption after a sidecar restart (several sessions resuming at once) reproduced it too
- A host left idle for hours reproduces nothing
So it looks load/timing related rather than input related.
What the logs show
- Sidecar log: no error, no warning at the moment the turn dies; the last entries are normal stream activity
- Process: sidecar is alive, no model child process activity, CPU idle
- Client: the session shows
busy;time.idleis never emitted for that turn - Sending a new prompt returns HTTP 200 (stored) and produces zero processing
Impact
For interactive use it is recoverable (restart), but for automation it is the worst failure mode: the queue silently accepts work that will never run, so a dispatcher that trusts the HTTP 200 looks healthy while doing nothing.
Requests
- Emit a terminal marker when a turn's stream dies. If the upstream stream ends without a finish/error signal, set the session back to
idle(or afailed/interruptedstate) with an error field, instead of leaving itbusyforever. A watchdog on the turn (no events for N seconds) would catch this. - Add abort/status endpoints.
POST /api/session/<id>/abortand a read-onlyGET /api/session/<id>/status(e.g.idle|busy|failed, current turn started-at, last event-at) would make detection and recovery possible without killing the process. - Make queued prompts recoverable. Either process them once the dead turn is cleared, or reject them explicitly while the session is stuck — anything is better than
200+ silence. - Optional: a client-visible heartbeat per running turn would let dispatchers detect wedging without polling the DB.
Workaround, for anyone hitting this
- Keep concurrency ≤2 per host and stagger overlapping work
- Detect + recover: kill the sidecar, respawn it from the desktop app, then continue in fresh sessions seeded with the previous progress (this recovery path worked 6/6 times here)
- Do not trust a
2xxfrom a prompt post as proof of processing
Happy to provide sidecar logs and a timestamped capture of a wedged session if that helps.
Related: #59556 (hang with AGENTS.md present) looks like a different trigger for a similar symptom — this report is specifically about the missing terminal state and the unusable queue afterwards.
Activity
opencode-agent commentedon Oct 27, 2026
Thanks for the detailed write-up and the workaround notes. I think this is a duplicate of #53747: a model step stalls mid-stream with no error, and the session stays busy until it's interrupted by hand. That issue has already been reproduced. When a provider keeps the stream open with SSE keepalive comments but sends no data, the chunk timeout never fires, so the turn never ends. Prompts sent after that are queued behind the stuck turn, which is why they never run.
Possible duplicates and related issues, ordered by likelihood:
running, with no timeout or reaperAGENTS.mdis present (different trigger, as you noted)On your request for endpoints, some of this already exists in 2.1.x under different paths, which is why you got 404s:
POST /api/session/<id>/interruptinterrupts the running turn and puts the session back to idle. With?resume=trueit continues with pending steering input, but queued prompts stay parked.GET /api/session/activelists sessions that are currently running, andGET /api/session/<id>returns a single session.This should let a dispatcher detect a stuck session and recover it without restarting the sidecar.
If your timestamped capture shows something #53747 doesn't cover, please add it there. If you think your problem is different, reply here and explain how. Otherwise the bot will close this issue as a duplicate in 1 day.