Skip to content

desktop: background shell reports Shell.NotFoundError after the local service restarts #57977

Open

Description

@kishimodo

Summary

A background shell launched through the shell tool was interrupted by a restart
of the local opencode service. The tool call then reported an internal
Shell.NotFoundError with no output file, and no log line explains that the
service went away or that the shell was lost.

Environment

  • opencode version: 2.0.27 (message="cli starting" version=2.0.27 channel=latest local=false args=["serve","--service"] role=cli)
  • OS: Windows 10.0.26200 (win32 x64)
  • Terminal: TERM=xterm-256color; TERM_PROGRAM/COLORTERM unset
  • Shell: C:\WINDOWS\system32\cmd.exe (tool commands run under Windows PowerShell)
  • Install/channel: desktop app (%LOCALAPPDATA%\Programs\@opencodedesktop\OpenCode.exe), CLI 2.0.27 under %APPDATA%\ai.opencode.desktop\cli\2.0.27
  • Active plugins: none found (no opencode.json in the workspace or ~/.config/opencode)

Reproduction

Observed once; not yet reproduced deliberately.

  1. In a long-running session, launch a multi-minute command as a background shell.
  2. While it is pending, the local service restarts (the app goes away and comes
    back, and the session resumes from a checkpoint).
  3. The pending tool call finishes as state="error" with the text
    Shell.NotFoundError, and the output path announced for that shell id does not
    exist on disk.

Expected Behavior

A tool call cut off by a service restart should say so ("the local service
restarted; this shell was interrupted"), and mark or re-run the shell instead of
surfacing an internal error type. The service lifecycle should appear in the log
(a stop or crash line), so the discontinuity is explainable.

Actual Behavior

  • Tool result verbatim:

    <shell id="sh_1611f9c450017wxSgDNEze4sKc" state="error" command="...">Shell.NotFoundError</shell>
    
  • The announced output file for that shell id was never created.

  • ~/.local/share/opencode/log/opencode.log: the previous service run
    (run=4ef7016f) ends at 2026-10-21T03:36:57.474Z on an ordinary
    spawning process line with no shutdown, error or crash line afterwards; the
    next run (run=2fee6bf5) begins at 2026-10-22T00:59:35.324Z with
    cli starting ... ["serve","--service"].

Additional Context

  • Frequency: once so far. Later background shells ran normally, which was also the workaround.
  • The session itself survived the restart (resumed from a checkpoint); only the
    in-flight shell call was lost.
  • If the service stopping was expected (the app was closed, or the machine slept),
    the issue is specifically the error surface plus the missing lifecycle log line.

Activity

  1. opencode-agent commented on Oct 22, 2026

    @opencode-agent
    Contributor

    Thanks for the report. I reproduced the Shell.NotFoundError result on 2.0.27 and on the latest dev (1b1ad48). I didn't need a service restart to get it.

    Steps:

    1. A session starts a background shell (sleep 600, background: true).
    2. While it's running, the shell is removed through the shell API (the same call the "kill shell" action makes).
    3. The session gets exactly what you saw: <shell id="sh_…" state="error" command="sleep 600">Shell.NotFoundError</shell>. The TUI shows "Shell failed", and the output file for that shell id is deleted.

    The recording and the script below show this.

    Going by the code, a real restart is handled differently: the service should report "Command cancelled because the server restarted" when it comes back up. So in your case the shell was probably removed or shut down inside the old service before it went away. I haven't looked into the missing stop or crash line in the log.

    Related:

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #57977: a background shell that is removed before it ends
    // reports the raw internal tag "Shell.NotFoundError" to the session.
    import { existsSync } from "node:fs"
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
      },
      ({ ui, llm, opencode }) =>
        Effect.gen(function* () {
          // Simulated model: 1) start a background shell, 2) acknowledge, 3) answer the
          // completion notification (we capture what the model is told there).
          let seen = ""
          yield* llm.serve((request: any) => {
            const body = JSON.stringify(request)
            if (!body.includes("call_bg"))
              return Stream.make(
                Llm.toolCall({
                  index: 0,
                  id: "call_bg",
                  name: "shell",
                  input: { command: "sleep 600", description: "long job", background: true },
                }),
                Llm.finish("tool-calls"),
              )
            if (!body.includes("<shell id=")) return Stream.make(Llm.text("Started the background job."))
            seen = body
            return Stream.make(Llm.text("Noted the shell result."))
          })
          yield* ui.submit("Run a long job in the background")
          yield* ui.waitFor("Started the background job.", { timeout: 25000 }).pipe(
            Effect.catch((e: any) =>
              Effect.log("WAITFAIL:\n" + (e.frame?.lines ?? []).map((l: any) => l.spans.map((s: any) => s.text).join("")).join("\n")).pipe(Effect.andThen(Effect.fail(e))),
            ),
          )
          const before: any = yield* ui.capture()
          yield* Effect.log("BEFORE:\n" + before.lines.map((l: any) => l.spans.map((s: any) => s.text).join("")).join("\n"))
          const shells: any = yield* opencode.shell.list()
          yield* Effect.log("shells: " + JSON.stringify(shells))
          const list = Array.isArray(shells) ? shells : (shells.items ?? shells.data ?? [])
          const id = list[0].id
          const file = list[0].file
          // Same thing the desktop/TUI "kill shell" action and other in-process removals do.
          yield* opencode.shell.remove({ id } as any)
          yield* Effect.sleep("4 seconds")
          yield* Effect.log(`output file ${file} exists after removal: ${existsSync(file)}`)
          yield* ui.screenshot("after-remove")
          const frame: any = yield* ui.capture()
          const text = frame.lines.map((l: any) => l.spans.map((s: any) => s.text).join("")).join("\n")
          yield* Effect.log("FRAME:\n" + text)
          const m = seen.match(/.{0,300}Shell\.NotFoundError.{0,100}/)
          yield* Effect.log("MODEL SAW: " + (m ? m[0] : "(no Shell.NotFoundError in model request)"))
          if (!m) return yield* Effect.fail(new Error("raw Shell.NotFoundError not shown"))
          yield* Effect.log("REPRODUCED: raw Shell.NotFoundError surfaced for removed background shell")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. jorgeferreira1 commented on Oct 25, 2026

    @jorgeferreira1

    A related variant on 2.1.4 (Linux TUI) where the session gets nothing, not even Shell.NotFoundError:

    1. A session starts a long background shell (background: true) and ends its turn. The session is now idle, waiting for the shell-finished notification.
    2. serve --service restarts. Here it was an auto-update 2.1.3 → 2.1.4 triggered by another TUI being opened.
    3. The new service answers GET /api/shell/<id>/output with 404, and the TUI shows "Shell cancelled". But no synthetic message is added to the session: the startup sweep only notifies sessions that were busy ("The server restarted while you were working"). The idle session's last record stays idle.

    So an agent that is waiting on a background shell (an overnight supervisor, in our case) is never woken again, and nothing in the session says why. Expected: a shell lost on restart completes with an error notification that wakes the owning session, whether it was busy or idle.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions