Repository navigation
jobs: background shell stuck running after process death; stale UI item cannot be cleared #56804
Copy link
Copy link
Open
Labels
Description
Description
A background shell job whose process terminated is never finalized by the daemon. It stays status: running in memory forever: the desktop session keeps showing it as "1 running" (still there 7+ hours later), its "Kill shell command" action does nothing, and no API can clear it. Only opencode service restart removes the ghost.
The persisted recovery record is stuck the same way (kv → job.background/<notificationID>, status unchanged since the spawn; no exit event was ever recorded).
Plugins
None related to shells (OpenCode Desktop build; one local project plugin for context resume).
OpenCode version
2.0.30 (bundled daemon opencode v2.0.30).
Steps to reproduce
- Start a long-running background shell from a session (shell tool with
background=true). Here:powershell -NoProfile -ExecutionPolicy Bypass -File <script>.ps1, spawn logged2026-10-18T06:48:15.795Z. - Let the process end without the normal close path. Here it was killed externally at ~
07:09Z(its own log stops at07:08:45Z, then it is gone from the process list). The daemon log shows the spawn line but no exit/termination line ever. - Observe the stuck state (verified hours later):
GET /api/shell/sh_14dc4a6b2001Hdh34Dm3AIylUi→404 ShellNotFoundErrorshell.listdoes not include it;DELETE(shell.remove) on the stale id → silent success, no state change — and the same silent success for bogus ids (so the UI kill action cannot even detect the shell is gone)- no
jobendpoints exist to complete/cancel a running job;session.inbox/session_pendingare empty - the UI item never self-heals; the kv record stays
"status":"running"indefinitely
Only a service restart clears it (in-memory state).
Expected behavior
- Finalize the job when the process exits for any reason (including external kill), and/or reconcile stale
runningjobs (e.g., at daemon startup). - Provide a real way to clear/cancel a stale running item;
shell.removefor an unknown id should 404 (likeshell.getdoes) instead of silently succeeding — the silent success is what makes the stale UI chip unclosable.
OS
Windows 11 (10.0.22631.6199, x64).
Additional context
- Evidence: kv record
job.background/msg_14dc4a6d1001d5S3mF8TGMP39k={"id":"sh_14dc4a6b2001Hdh34Dm3AIylUi","notificationID":"msg_14dc4a6d1001d5S3mF8TGMP39k","recovery":{"kind":"shell","sessionID":"ses_…","shellID":"sh_14dc4a6b2001Hdh34Dm3AIylUi","command":"powershell -NoProfile -ExecutionPolicy Bypass -File \"<tmp>\\<script>.ps1\""},"status":"running"}(unchanged since spawn). - Job completion appears to rely solely on the process-exit event wired when the job starts; if that event is missed there is no reaper and nothing else can move the job out of
running. - Possibly related: v2: shell: no way to stop a running background command #36424 (no way to stop a running background command), shell: detached Windows process leaves parent session stuck running #43910 (closed; detached Windows process leaves parent session stuck running — workaround
POST /api/session/{id}/background), server: PID 1 does not reap orphaned grandchildren, leaving zombies and shells stuck "running" #53564 (shells stuck "running"), V2: define restart semantics for foreground and background shells #36348 (v2 shell restart semantics). - Workaround used:
opencode service restart, plus a UI refresh.
Reactions are currently unavailable
Activity
opencode-agent commentedon Oct 18, 2026
Thanks for the detailed report. I reproduced this on 2.0.30, and it still happens on the latest
dev(da9d436).Steps (Linux, simulated model):
sleep 600,background: true).DELETE /api/debug/location. That's the same path the 60-minute idle eviction uses ([Bug] Location inactivity eviction SIGTERMs live background shells after 60 minutes #51828).Result:
GET /api/shell/{id}returns 404ShellNotFoundErrorandshell.listis empty.DELETE /api/shell/<any unknown id>returns success (204).The recording and script below show this.
Two things I couldn't confirm:
kill -9is handled correctly; the shell shows as exited and the "Shell finished" notification arrives. So in your case the shell record most likely disappeared some other way, such as the location being torn down. I can't test a Windows-specific kill path here.job.backgroundrecord: I couldn't inspect it. From the code, a job that ends this way is markedcancelled, and cancelled jobs aren't written back to that record, so it would stayrunning. That matches what you saw.Related: #51828 (idle eviction kills background shells) and #56729 / #56780 (background shell completion notifications lost when the shell plugin is rebuilt).
recording.mp4
Reproduction script (drive-evict.ts)
Run it with opencode-drive:
opencode-drive run ./drive-evict.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.reynold-hu commentedon Oct 18, 2026
Opened #56809 for the API half:
DELETE /api/shell/:idnow returns 404ShellNotFoundErrorfor an unknown id (the endpoint declares the error too), matchingshell.get. The test fails on the parent commit with 204. I also confirmed from the code that explicitjobs.cancelpersists a cancelled background marker, while the teardown path (scope close) marks the job cancelled without writing that state back - the marker then stays running until restart recovery. Settling the session timeline when a Location is evicted looks like the remaining piece; happy to follow up if a maintainer points at the preferred owner.reynold-hu commentedon Oct 19, 2026
Following the review on #56809: I closed that API change because the idempotent 204 is an intentional contract (V2_HTTP_API_AUDIT entry 135). The remaining fix is settling the session/timeline state when a Location is torn down, so the ghost chip clears without a shell removal. From the code, explicit
jobs.cancelpersists a cancelled marker while the scope-close path insettleskips that write; that marker mismatch is separate from the client's stuck row.