From 9aa0675611bd6352e9c0361db219836e44ea7460 Mon Sep 17 00:00:00 2001 From: Zubeen Tolani Date: Sun, 22 Feb 2026 13:08:26 +0530 Subject: [PATCH 1/5] fix(client): add request timeout to get_path() to prevent hanging on unresponsive processes --- lua/opencode/cli/client.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index e5ac31f8..e7ecec2c 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -46,8 +46,10 @@ end ---@param body table? ---@param on_success fun(response: table)? ---@param on_error fun(code: number, msg: string?)? +---@param opts? { max_time?: number } Optional settings (max_time limits total request time in seconds) ---@return number job_id -local function curl(url, method, body, on_success, on_error) +local function curl(url, method, body, on_success, on_error, opts) + opts = opts or {} local command = { "curl", "-s", @@ -64,6 +66,11 @@ local function curl(url, method, body, on_success, on_error) "-N", -- No buffering, for streaming SSEs } + if opts.max_time then + table.insert(command, "--max-time") + table.insert(command, tostring(opts.max_time)) + end + if body then table.insert(command, "-d") table.insert(command, vim.fn.json_encode(body)) @@ -149,9 +156,10 @@ end ---@param body table? ---@param on_success fun(response: table)? ---@param on_error fun(code: number, msg: string?)? +---@param opts? { max_time?: number } Optional settings (max_time limits total request time in seconds) ---@return number job_id -function M.call(port, path, method, body, on_success, on_error) - return curl("http://localhost:" .. port .. path, method, body, on_success, on_error) +function M.call(port, path, method, body, on_success, on_error, opts) + return curl("http://localhost:" .. port .. path, method, body, on_success, on_error, opts) end ---@param text string @@ -269,7 +277,7 @@ end ---@param on_success fun(response: opencode.cli.client.PathResponse) ---@param on_error fun() function M.get_path(port, on_success, on_error) - M.call(port, "/path", "GET", nil, on_success, on_error) + M.call(port, "/path", "GET", nil, on_success, on_error, { max_time = 2 }) end ---@class opencode.cli.client.Event From 279fd176cd234d4135b89e37f561ea46cec74f03 Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Fri, 27 Feb 2026 06:34:44 -0700 Subject: [PATCH 2/5] apply max_time to all non-SSE requests --- lua/opencode/cli/client.lua | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index e6ce974f..30790b19 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -46,7 +46,7 @@ end ---@param body table? ---@param on_success fun(response: table)? ---@param on_error fun(code: number, msg: string?)? ----@param opts? { max_time?: number } Optional settings (max_time limits total request time in seconds) +---@param opts? { max_time?: number } ---@return number job_id local function curl(url, method, body, on_success, on_error, opts) opts = opts or {} @@ -163,10 +163,16 @@ end ---@param body table? ---@param on_success fun(response: table)? ---@param on_error fun(code: number, msg: string?)? ----@param opts? { max_time?: number } Optional settings (max_time limits total request time in seconds) +---@param opts? { max_time?: number } ---@return number job_id function M.call(port, path, method, body, on_success, on_error, opts) - -- TODO: wraps `curl` unnecessarily + opts = opts + or { + -- `opencode` server is unresponsive when its job is suspended in the background. + -- Time out rather than hang indefinitely. + max_time = 2, + } + -- TODO: wraps `curl` unnecessarily return curl("http://localhost:" .. port .. path, method, body, on_success, on_error, opts) end @@ -311,7 +317,10 @@ end ---@param on_error fun(code: number, msg: string?)|nil ---@return number job_id function M.sse_subscribe(port, on_success, on_error) - return M.call(port, "/event", "GET", nil, on_success, on_error) + return M.call(port, "/event", "GET", nil, on_success, on_error, { + -- Keep connection open indefinitely to continue receiving + max_time = 0, + }) end return M From ecd7116d4d68ee67eddfeadca47a0dd7b005df85 Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Fri, 27 Feb 2026 06:34:49 -0700 Subject: [PATCH 3/5] remove connect-timeout --- lua/opencode/cli/client.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index 30790b19..451c087a 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -53,8 +53,6 @@ local function curl(url, method, body, on_success, on_error, opts) local command = { "curl", "-s", - "--connect-timeout", - "1", "-X", method, "-H", From 4d90f3d26808d411182c291d1e25c65e00eb79ea Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Fri, 27 Feb 2026 06:38:31 -0700 Subject: [PATCH 4/5] dont need explicit here --- lua/opencode/cli/client.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index 451c087a..25d40769 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -289,7 +289,7 @@ end ---@param on_success fun(response: opencode.cli.client.PathResponse) ---@param on_error fun() function M.get_path(port, on_success, on_error) - M.call(port, "/path", "GET", nil, on_success, on_error, { max_time = 2 }) + M.call(port, "/path", "GET", nil, on_success, on_error) end ---@alias opencode.cli.client.event.type From 4d1e9a0c33526a7c09be54385ccf0155c6a24456 Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Fri, 27 Feb 2026 06:47:02 -0700 Subject: [PATCH 5/5] remove pass-through M.call --- lua/opencode/cli/client.lua | 66 +++++++++++++++---------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/lua/opencode/cli/client.lua b/lua/opencode/cli/client.lua index 25d40769..f6e58dda 100644 --- a/lua/opencode/cli/client.lua +++ b/lua/opencode/cli/client.lua @@ -41,15 +41,23 @@ local function generate_uuid() ) end ----@param url string ----@param method string +---@param port number +---@param path string +---@param method "GET"|"POST" ---@param body table? ----@param on_success fun(response: table)? ----@param on_error fun(code: number, msg: string?)? +---@param on_success? fun(response: table) +---@param on_error? fun(code: number, msg: string?) ---@param opts? { max_time?: number } ---@return number job_id -local function curl(url, method, body, on_success, on_error, opts) - opts = opts or {} +local function curl(port, path, method, body, on_success, on_error, opts) + local url = "http://localhost:" .. port .. path + opts = opts + or { + -- `opencode` server is unresponsive when its job is suspended in the background. + -- Time out rather than hang indefinitely. + max_time = 2, + } + local command = { "curl", "-s", @@ -154,38 +162,18 @@ local function curl(url, method, body, on_success, on_error, opts) }) end ----Call an opencode server endpoint. ----@param port number ----@param path string ----@param method "GET"|"POST" ----@param body table? ----@param on_success fun(response: table)? ----@param on_error fun(code: number, msg: string?)? ----@param opts? { max_time?: number } ----@return number job_id -function M.call(port, path, method, body, on_success, on_error, opts) - opts = opts - or { - -- `opencode` server is unresponsive when its job is suspended in the background. - -- Time out rather than hang indefinitely. - max_time = 2, - } - -- TODO: wraps `curl` unnecessarily - return curl("http://localhost:" .. port .. path, method, body, on_success, on_error, opts) -end - ---@param text string ---@param port number ---@param callback fun(response: table)|nil function M.tui_append_prompt(text, port, callback) - M.call(port, "/tui/publish", "POST", { type = "tui.prompt.append", properties = { text = text } }, callback) + curl(port, "/tui/publish", "POST", { type = "tui.prompt.append", properties = { text = text } }, callback) end ---@param command opencode.Command|string ---@param port number ---@param callback fun(response: table)|nil function M.tui_execute_command(command, port, callback) - M.call(port, "/tui/publish", "POST", { type = "tui.command.execute", properties = { command = command } }, callback) + curl(port, "/tui/publish", "POST", { type = "tui.command.execute", properties = { command = command } }, callback) end ---@param prompt string @@ -208,7 +196,7 @@ function M.send_message(prompt, session_id, port, provider_id, model_id, callbac }, } - M.call(port, "/session/" .. session_id .. "/message", "POST", body, callback) + curl(port, "/session/" .. session_id .. "/message", "POST", body, callback) end ---@param port number @@ -216,7 +204,7 @@ end ---@param reply "once"|"always"|"reject" ---@param callback? fun(session: table) function M.permit(port, permission, reply, callback) - M.call(port, "/permission/" .. permission .. "/reply", "POST", { + curl(port, "/permission/" .. permission .. "/reply", "POST", { reply = reply, }, callback) end @@ -229,7 +217,7 @@ end ---@param port number ---@param callback fun(agents: opencode.cli.client.Agent[]) function M.get_agents(port, callback) - M.call(port, "/agent", "GET", nil, callback) + curl(port, "/agent", "GET", nil, callback) end ---@class opencode.cli.client.Command @@ -243,7 +231,7 @@ end ---@param port number ---@param callback fun(commands: opencode.cli.client.Command[]) function M.get_commands(port, callback) - M.call(port, "/command", "GET", nil, callback) + curl(port, "/command", "GET", nil, callback) end ---@class opencode.cli.client.SessionTime @@ -260,7 +248,7 @@ end ---@param port number ---@param callback fun(sessions: opencode.cli.client.Session[]) function M.get_sessions(port, callback) - M.call(port, "/session", "GET", nil, callback) + curl(port, "/session", "GET", nil, callback) end ---@class opencode.cli.client.SessionStatus @@ -270,7 +258,7 @@ end ---@param port number ---@param callback fun(statuses: opencode.cli.client.SessionStatus[]) function M.get_sessions_status(port, callback) - M.call(port, "/session/status", "GET", nil, callback) + curl(port, "/session/status", "GET", nil, callback) end ---Select session in `opencode`. @@ -278,7 +266,7 @@ end ---@param port number ---@param session_id string function M.select_session(port, session_id) - M.call(port, "/tui/select-session", "POST", { sessionID = session_id }, nil) + curl(port, "/tui/select-session", "POST", { sessionID = session_id }, nil) end ---@class opencode.cli.client.PathResponse @@ -289,7 +277,7 @@ end ---@param on_success fun(response: opencode.cli.client.PathResponse) ---@param on_error fun() function M.get_path(port, on_success, on_error) - M.call(port, "/path", "GET", nil, on_success, on_error) + curl(port, "/path", "GET", nil, on_success, on_error) end ---@alias opencode.cli.client.event.type @@ -308,14 +296,12 @@ end ---@field type opencode.cli.client.event.type|string ---@field properties table ----Calls the `/event` SSE endpoint and invokes `callback` for each event received. ---- ---@param port number ----@param on_success fun(response: opencode.cli.client.Event)|nil +---@param on_success fun(response: opencode.cli.client.Event)|nil Invoked with each received event. ---@param on_error fun(code: number, msg: string?)|nil ---@return number job_id function M.sse_subscribe(port, on_success, on_error) - return M.call(port, "/event", "GET", nil, on_success, on_error, { + return curl(port, "/event", "GET", nil, on_success, on_error, { -- Keep connection open indefinitely to continue receiving max_time = 0, })