From c0f98ee355299a53e379e2b0164782c2604a7a5e Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Tue, 10 Feb 2026 08:08:40 -0500 Subject: [PATCH 1/4] feat(logging): add configurable logging support to opencode --- README.md | 5 ++++ lua/opencode/config.lua | 5 ++++ lua/opencode/log.lua | 54 +++++++++++++++++++++++++++++++++++++++++ lua/opencode/types.lua | 6 +++++ 4 files changed, 70 insertions(+) create mode 100644 lua/opencode/log.lua diff --git a/README.md b/README.md index a865ff4e..7e87a39c 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,11 @@ require('opencode').setup({ enabled = false, }, }, + logging = { + enabled = false, + level = 'warn', -- debug, info, warn, error + outfile = nil, + }, debug = { enabled = false, -- Enable debug messages in the output window capture_streamed_events = false, diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index 14d131ff..21f58875 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -216,6 +216,11 @@ M.defaults = { enabled = false, }, }, + logging = { + enabled = false, + level = 'info', -- debug, info, warn, error + outfile = nil, + }, debug = { enabled = false, capture_streamed_events = false, diff --git a/lua/opencode/log.lua b/lua/opencode/log.lua new file mode 100644 index 00000000..a19a70c1 --- /dev/null +++ b/lua/opencode/log.lua @@ -0,0 +1,54 @@ +local M = {} + +local config = require('opencode.config') +local log_path = config.logging and config.logging.outfile or vim.fn.stdpath('log') .. '/opencode.log' +local level = config.logging and config.logging.level or 'warn' + +local logger = require('plenary.log').new({ + plugin = 'opencode', + level = level:lower(), + use_console = false, + outfile = log_path, +}) + +local function get_logger() + return logger +end + +function M.debug(msg, ...) + if not config.logging.enabled then + return + end + get_logger().debug(string.format(msg, ...)) +end + +function M.info(msg, ...) + if not config.logging.enabled then + return + end + get_logger().info(string.format(msg, ...)) +end + +function M.warn(msg, ...) + if not config.logging.enabled then + return + end + get_logger().warn(string.format(msg, ...)) +end + +function M.error(msg, ...) + if not config.logging.enabled then + return + end + get_logger().error(string.format(msg, ...)) +end + +--- @return string +function M.get_path() + if not config.logging.enabled then + return + end + return log_path +end + +return M diff --git a/lua/opencode/types.lua b/lua/opencode/types.lua index 69067c13..7763b366 100644 --- a/lua/opencode/types.lua +++ b/lua/opencode/types.lua @@ -189,6 +189,11 @@ ---@field default_agent? string -- Use current mode if nil ---@field instructions? string[] -- Custom instructions for quick chat +---@class OpencodeLoggingConfig +---@field enabled boolean +---@field level 'debug' | 'info' | 'warn' | 'error' +---@field outfile string|nil + ---@class OpencodeConfig ---@field preferred_picker 'telescope' | 'fzf' | 'mini.pick' | 'snacks' | 'select' | nil ---@field preferred_completion 'blink' | 'nvim-cmp' | 'vim_complete' | nil -- Preferred completion strategy for mentons and commands @@ -200,6 +205,7 @@ ---@field keymap OpencodeKeymap ---@field ui OpencodeUIConfig ---@field context OpencodeContextConfig +---@field logging OpencodeLoggingConfig ---@field debug OpencodeDebugConfig ---@field prompt_guard? fun(mentioned_files: string[]): boolean ---@field hooks OpencodeHooks From 89373f578742b8ca9eb745f3ab5d2ff95712fc5b Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Mon, 9 Feb 2026 10:26:01 -0500 Subject: [PATCH 2/4] fix(server): improve shutdown sequence to ensure reliable job termination - Remove wait duration from autocmd shutdown call - Implement graceful SIGTERM with fallback to SIGKILL if needed This should fix #254 and #251 --- lua/opencode/opencode_server.lua | 46 ++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/lua/opencode/opencode_server.lua b/lua/opencode/opencode_server.lua index ea5f6116..cb4995f3 100644 --- a/lua/opencode/opencode_server.lua +++ b/lua/opencode/opencode_server.lua @@ -25,7 +25,7 @@ local function ensure_vim_leave_autocmd() local state = require('opencode.state') if state.opencode_server then pcall(function() - state.opencode_server:shutdown():wait(2000) + state.opencode_server:shutdown() end) end end, @@ -50,32 +50,44 @@ function OpencodeServer:is_running() return self.job and self.job.pid ~= nil end ---- Clean up this server job ---- @return Promise function OpencodeServer:shutdown() if self.shutdown_promise:is_resolved() then return self.shutdown_promise end - if self.job and self.job.pid then - local job = self.job + if not self.job or not self.job.pid then + self.shutdown_promise:resolve(true) + return self.shutdown_promise + end - self.job = nil - self.url = nil - self.handle = nil + local job = self.job + local pid = job.pid + self.job = nil + self.url = nil + self.handle = nil + + -- Graceful shutdown (SIGTERM) + pcall(function() + vim.uv.kill(pid, 15) + end) + + local exited = vim.wait(500, function() + return self.shutdown_promise:is_resolved() + end, 50) + + -- Forceful shutdown (SIGKILL) if not exited after waiting + if not exited then pcall(function() - job:kill(15) -- SIGTERM + vim.uv.kill(pid, 9) end) - vim.defer_fn(function() - if job and job.pid then - pcall(function() - job:kill(9) -- SIGKILL - end) - end - end, 500) - else + vim.wait(200, function() + return self.shutdown_promise:is_resolved() + end, 50) + end + + if not self.shutdown_promise:is_resolved() then self.shutdown_promise:resolve(true) end From 7ebe843b390edd3a20f489fec401b201066a95af Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Tue, 10 Feb 2026 08:10:07 -0500 Subject: [PATCH 3/4] fix(server): improve shutdown logic, add logs for SIGTERM/SIGKILL --- lua/opencode/opencode_server.lua | 53 ++++++++++++-------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/lua/opencode/opencode_server.lua b/lua/opencode/opencode_server.lua index cb4995f3..c962317a 100644 --- a/lua/opencode/opencode_server.lua +++ b/lua/opencode/opencode_server.lua @@ -23,10 +23,9 @@ local function ensure_vim_leave_autocmd() group = vim.api.nvim_create_augroup('OpencodeVimLeavePre', { clear = true }), callback = function() local state = require('opencode.state') + local log = require('opencode.log') if state.opencode_server then - pcall(function() - state.opencode_server:shutdown() - end) + state.opencode_server:shutdown() end end, }) @@ -35,6 +34,7 @@ end --- Create a new ServerJob instance --- @return OpencodeServer function OpencodeServer.new() + local log = require('opencode.log') ensure_vim_leave_autocmd() return setmetatable({ @@ -51,46 +51,28 @@ function OpencodeServer:is_running() end function OpencodeServer:shutdown() + local log = require('opencode.log') if self.shutdown_promise:is_resolved() then return self.shutdown_promise end - if not self.job or not self.job.pid then - self.shutdown_promise:resolve(true) - return self.shutdown_promise - end - - local job = self.job - local pid = job.pid - - self.job = nil - self.url = nil - self.handle = nil - - -- Graceful shutdown (SIGTERM) - pcall(function() - vim.uv.kill(pid, 15) - end) + if self.job and self.job.pid then + local pid = self.job.pid - local exited = vim.wait(500, function() - return self.shutdown_promise:is_resolved() - end, 50) - - -- Forceful shutdown (SIGKILL) if not exited after waiting - if not exited then - pcall(function() - vim.uv.kill(pid, 9) - end) + self.job = nil + self.url = nil + self.handle = nil - vim.wait(200, function() - return self.shutdown_promise:is_resolved() - end, 50) - end + local ok_term, err_term = pcall(vim.uv.kill, pid, 15) + log.debug('shutdown: SIGTERM pid=%d ok=%s err=%s', pid, tostring(ok_term), tostring(err_term)) - if not self.shutdown_promise:is_resolved() then - self.shutdown_promise:resolve(true) + local ok_kill, err_kill = pcall(vim.uv.kill, pid, 9) + log.debug('shutdown: SIGKILL pid=%d ok=%s err=%s', pid, tostring(ok_kill), tostring(err_kill)) + else + log.debug('shutdown: no job running') end + self.shutdown_promise:resolve(true) return self.shutdown_promise end @@ -105,6 +87,7 @@ end --- @return Promise function OpencodeServer:spawn(opts) opts = opts or {} + local log = require('opencode.log') self.job = vim.system({ config.opencode_executable, @@ -122,6 +105,7 @@ function OpencodeServer:spawn(opts) self.url = url self.spawn_promise:resolve(self) safe_call(opts.on_ready, self.job, url) + log.debug('spawn: server ready at url=%s', url) end end end, @@ -154,6 +138,7 @@ function OpencodeServer:spawn(opts) self.handle = self.job and self.job.pid + log.debug('spawn: started job with pid=%s', tostring(self.job and self.job.pid)) return self.spawn_promise end From 4353dcaee3a64d776e60b1c759c28ed262c284d8 Mon Sep 17 00:00:00 2001 From: Francis Belanger Date: Tue, 10 Feb 2026 11:08:01 -0500 Subject: [PATCH 4/4] fix(server): improve shutdown sequence to properly terminate child processes - Refactors shutdown to send SIGTERM to child processes before SIGKILL - Adds detailed debug logging for process termination - Ensures resources are cleaned up consistently --- lua/opencode/opencode_server.lua | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lua/opencode/opencode_server.lua b/lua/opencode/opencode_server.lua index c962317a..e63a1b6d 100644 --- a/lua/opencode/opencode_server.lua +++ b/lua/opencode/opencode_server.lua @@ -50,6 +50,13 @@ function OpencodeServer:is_running() return self.job and self.job.pid ~= nil end +local function kill_process(pid, signal, desc) + local log = require('opencode.log') + local ok, err = pcall(vim.uv.kill, pid, signal) + log.debug('shutdown: %s pid=%d sig=%d ok=%s err=%s', desc, pid, signal, tostring(ok), tostring(err)) + return ok, err +end + function OpencodeServer:shutdown() local log = require('opencode.log') if self.shutdown_promise:is_resolved() then @@ -57,21 +64,27 @@ function OpencodeServer:shutdown() end if self.job and self.job.pid then + ---@cast self.job vim.SystemObj local pid = self.job.pid + local children = vim.api.nvim_get_proc_children(pid) - self.job = nil - self.url = nil - self.handle = nil + if #children > 0 then + log.debug('shutdown: process pid=%d has %d children (%s)', pid, #children, vim.inspect(children)) - local ok_term, err_term = pcall(vim.uv.kill, pid, 15) - log.debug('shutdown: SIGTERM pid=%d ok=%s err=%s', pid, tostring(ok_term), tostring(err_term)) + for _, cid in ipairs(children) do + kill_process(cid, 15, 'SIGTERM child') + end + end - local ok_kill, err_kill = pcall(vim.uv.kill, pid, 9) - log.debug('shutdown: SIGKILL pid=%d ok=%s err=%s', pid, tostring(ok_kill), tostring(err_kill)) + kill_process(pid, 15, 'SIGTERM') + kill_process(pid, 9, 'SIGKILL') else log.debug('shutdown: no job running') end + self.job = nil + self.url = nil + self.handle = nil self.shutdown_promise:resolve(true) return self.shutdown_promise end