Skip to content

After the neovim process ends, the opencode serve process does not close accordingly #251

Closed

Description

@widdsun

Every time I open and close neovim, an opencode serve process spawned by this plugin is left running, which causes excessive memory usage. It seems the only solution is to manually terminate the opencode serve process.
I'm not sure if this is intented behavior or a bug. Can the same opencode serve be reused by multiple neovim instances?

Image

Activity

  1. sudo-tee commented on Feb 7, 2026

    @sudo-tee
    Owner

    Thanks for the report.

    This is a puzzling issue as it does not happen to everyone and I am not able to reproduce it yet

    I will have another look

    I might need some help debugging it.

  2. sudo-tee commented on Feb 9, 2026

    @sudo-tee
    Owner

    @widdsun

    I played with the shutdown of the server in a PR, please let me know if it resolves the issue for you. As I was not experiencing this issue

    #255

  3. pirey commented on Feb 10, 2026

    @pirey
    Contributor

    @sudo-tee while I tried the branch, I found out this particular issue still persists

    the opencode serve process was not killed after exit, and opening neovim will spawn another process.

    Screen.Recording.2026-02-10.at.08.28.34.mp4
  4. sudo-tee commented on Feb 10, 2026

    @sudo-tee
    Owner

    What OS are you using?

    Do you have a link to your dotfiles ?

    I seriously don't know why it's happening but only for few people

  5. pirey commented on Feb 10, 2026

    @pirey
    Contributor

    I am using macos running neovim nightly release.
    here's my dotfiles and neovim config
    https://github.com/pirey/dotfiles/tree/master/config-files/home
    https://github.com/pirey/nvim

  6. sudo-tee commented on Feb 10, 2026

    @sudo-tee
    Owner

    @pirey @widdsun

    I simplified the shutdown logic to the minimum and added some logging

    you can enable it in your config

    {
       logging {
          enabled = true,
          level = 'debug'
       }
    }

    Could you do me a favor and enable the log even if it works for you so I can see if there is an error in one of the SIGTERM or SIGKILL ?

    file path should be ~/.local/state/nvim/opencode.log

    Thank you very much

  7. pirey commented on Feb 10, 2026

    @pirey
    Contributor

    I found something. @sudo-tee

    I tried to monitor the spawned opencode process (using btop), and apparently the spawned process had a child process.
    And when I exit neovim, the cleanup function only kill the parent process, leaving the child process orphaned.

    Screen.Recording.2026-02-10.at.21.33.34.mov

    Here's a screenshot with the full command

    Image
  8. pirey commented on Feb 10, 2026

    @pirey
    Contributor

    Here's the log file that you mentioned.
    I cleared some earlier logs, though. I opened and close neovim and opencode several times after that.

    [DEBUG Tue Feb 10 21:33:12 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: started job with pid=36186
    [DEBUG Tue Feb 10 21:33:13 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: server ready at url=http://127.0.0.1:4096
    [DEBUG Tue Feb 10 21:33:18 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: shutdown: SIGTERM pid=36186 ok=true err=0
    [DEBUG Tue Feb 10 21:33:18 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: shutdown: SIGKILL pid=36186 ok=true err=0
    [DEBUG Tue Feb 10 21:33:39 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: started job with pid=36369
    [DEBUG Tue Feb 10 21:33:39 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: server ready at url=http://127.0.0.1:4096
    [DEBUG Tue Feb 10 21:33:44 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: shutdown: SIGTERM pid=36369 ok=true err=0
    [DEBUG Tue Feb 10 21:33:44 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: shutdown: SIGKILL pid=36369 ok=true err=0
    [DEBUG Tue Feb 10 21:37:04 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: started job with pid=36550
    [DEBUG Tue Feb 10 21:37:04 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: server ready at url=http://127.0.0.1:51687
    [DEBUG Tue Feb 10 21:37:07 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: shutdown: SIGTERM pid=36550 ok=true err=0
    [DEBUG Tue Feb 10 21:37:07 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: shutdown: SIGKILL pid=36550 ok=true err=0
    [DEBUG Tue Feb 10 21:37:11 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: started job with pid=36697
    [DEBUG Tue Feb 10 21:37:11 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: server ready at url=http://127.0.0.1:51733
    [DEBUG Tue Feb 10 21:37:13 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: shutdown: SIGTERM pid=36697 ok=true err=0
    [DEBUG Tue Feb 10 21:37:13 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: shutdown: SIGKILL pid=36697 ok=true err=0
    [DEBUG Tue Feb 10 21:37:49 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: started job with pid=36920
    [DEBUG Tue Feb 10 21:37:50 2026] /Users/yeri/.local/share/nvim/site/pack/core/opt/opencode.nvim/lua/opencode/log.lua:22: spawn: server ready at url=http://127.0.0.1:4096
    
  9. sudo-tee commented on Feb 10, 2026

    @sudo-tee
    Owner

    @pirey

    Thanks for the clarification.

    I added a check for child process before killing it.

    Let me know it it works properly for you now

  10. pirey commented on Feb 10, 2026

    @pirey
    Contributor

    Hey thanks.

    I can confirm that the spawned process and its child has been terminated now.

    Works well on my end.

    Screen.Recording.2026-02-10.at.23.33.10.mov
  11. sudo-tee commented on Feb 10, 2026

    @sudo-tee
    Owner

    Awesome it looks like it solves your other issue at the same time.

    I will merge this :)

  12. Dimfred commented on May 5, 2026

    @Dimfred
    Contributor

    I don't know how it is for others, but I seem to still experience those issues.

    Running this on exit now which seems to work:

    M = {}
    
    ---@param path string
    ---@return table<string, table>|nil
    local function read_port_mappings(path)
      local file = io.open(path, "r")
      if not file then
        return nil
      end
    
      local content = file:read("*all")
      file:close()
    
      local ok, mappings = pcall(vim.json.decode, content or "")
      if not ok or type(mappings) ~= "table" then
        return nil
      end
    
      return mappings
    end
    
    ---@param mapping table|nil
    ---@return boolean
    local function mapping_has_current_client(mapping)
      if not mapping or type(mapping.nvim_pids) ~= "table" then
        return false
      end
    
      local current_pid = vim.fn.getpid()
      for _, client in ipairs(mapping.nvim_pids) do
        if client.pid == current_pid then
          return true
        end
      end
    
      return false
    end
    
    ---@return integer|nil
    local function current_opencode_server_pid()
      local state = require("opencode.state")
      local server = state.opencode_server
      if not server then
        return nil
      end
    
      if server.job and server.job.pid then
        return server.job.pid
      end
    
      if not server.port then
        return nil
      end
    
      local mappings = read_port_mappings(vim.fn.stdpath("data") .. "/opencode_port_mappings.json")
      local mapping = mappings and mappings[tostring(server.port)]
      if not mapping or not mapping_has_current_client(mapping) then
        return nil
      end
    
      return mapping.server_pid
    end
    
    ---Kill the opencode server owned by this Neovim instance on exit.
    function M.shutdown_current()
      local pid = current_opencode_server_pid()
      if not pid then
        return
      end
    
      require("utils.pid").kill_tree(pid)
    end
    
    return M
    local M = {}
    
    ---@param pid integer|nil
    ---@return boolean
    function M.alive(pid)
      return type(pid) == "number" and vim.uv.kill(pid, 0) == 0
    end
    
    ---@param pid integer
    local function kill_tree(pid)
      if not M.alive(pid) then
        return
      end
    
      local ok, children = pcall(vim.api.nvim_get_proc_children, pid)
      if ok and type(children) == "table" then
        for _, child in ipairs(children) do
          kill_tree(child)
        end
      end
    
      pcall(vim.uv.kill, pid, 15)
      if M.alive(pid) then
        pcall(vim.uv.kill, pid, 9)
      end
    end
    M.kill_tree = kill_tree
    
    return M
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions