Skip to content

Commit a8956e1

Browse files
committed
fix(core): restart opencode server on 3 cancels
If the user tries to cancel their request three times in a row, shutdown the opencode server and then restart. Getting the code working was a little bit tricky for two main issues: 1. If we try to do it on demand in api_client when the user sends a prompt, then there's a race condition between that prompt getting sent and when we get subscribed to server events. That race condition can cause us to miss message/part events. 2. Some other listeners to opencode_server may trigger api_client calls so we have to be extra careful to make sure base_url is set. That means we can't rely solely on state.opencode_server notification to update base_url but have to check it before using it.
1 parent 1bd3bc9 commit a8956e1

File tree

2 files changed

+72
-26
lines changed

2 files changed

+72
-26
lines changed

lua/opencode/api_client.lua

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,42 @@ function OpencodeApiClient.new(base_url)
1414
}, OpencodeApiClient)
1515
end
1616

17+
---Ensure that base_url is set. Even thought we're subscribed to
18+
---state.opencode_server, we still need this check because
19+
---it's possible someone will try to make an api call in their event
20+
---handler (e.g. event_manager or header)
21+
---@return boolean
22+
function OpencodeApiClient:_ensure_base_url()
23+
-- NOTE: eventhough we're subscribed opencode_server, we need this check for
24+
-- base_url because the notification about opencode_server being set to
25+
-- non-nil my not have gotten to us in time
26+
if self.base_url then
27+
return true
28+
end
29+
30+
local state = require('opencode.state')
31+
32+
if not state.opencode_server then
33+
state.opencode_server = server_job.ensure_server() --[[@as OpencodeServer]]
34+
-- shouldn't normally happen but prevents error in replay tester
35+
if not state.opencode_server then
36+
return false
37+
end
38+
end
39+
40+
self.base_url = state.opencode_server.url:gsub('/$', '')
41+
return true
42+
end
43+
1744
--- Make a typed API call
1845
--- @param endpoint string The API endpoint path
1946
--- @param method string|nil HTTP method (default: 'GET')
2047
--- @param body table|nil|boolean Request body
2148
--- @param query table|nil Query parameters
2249
--- @return Promise<any> promise
2350
function OpencodeApiClient:_call(endpoint, method, body, query)
24-
if not self.base_url then
25-
local state = require('opencode.state')
26-
state.opencode_server = server_job.ensure_server() --[[@as OpencodeServer]]
27-
-- shouldn't normally happen but prevents error in replay tester
28-
if not state.opencode_server then
29-
return nil
30-
end
31-
self.base_url = state.opencode_server.url:gsub('/$', '')
51+
if not self:_ensure_base_url() then
52+
return nil
3253
end
3354
local url = self.base_url .. endpoint
3455

@@ -361,10 +382,7 @@ end
361382
--- @param on_event fun(event: table) Event callback
362383
--- @return Job The streaming job handle
363384
function OpencodeApiClient:subscribe_to_events(directory, on_event)
364-
if not self.base_url then
365-
local state = require('opencode.state')
366-
self.base_url = state.opencode_server.url:gsub('/$', '')
367-
end
385+
self:_ensure_base_url()
368386
local url = self.base_url .. '/event'
369387
if directory then
370388
url = url .. '?directory=' .. directory
@@ -403,13 +421,31 @@ function OpencodeApiClient:list_tools(provider, model, directory)
403421
end
404422

405423
--- Create a factory function for the module
406-
--- @param base_url string The base URL of the opencode server
424+
--- @param base_url? string The base URL of the opencode server
407425
--- @return OpencodeApiClient
408426
local function create_client(base_url)
409-
return OpencodeApiClient.new(base_url)
410-
end
427+
local state = require('opencode.state')
428+
429+
base_url = base_url or state.opencode_server and state.opencode_server.url
411430

412-
local function instance() end
431+
local api_client = OpencodeApiClient.new(base_url)
432+
433+
local function on_server_change(_, new_val, _)
434+
-- NOTE: set base_url here if we can. we still need the check in _call
435+
-- because the event firing on the server change may not have happened
436+
-- before a caller is trying to make an api request, so the main benefit
437+
-- of the subscription is setting base_url to nil when the server goes away
438+
if new_val and new_val.url then
439+
api_client.base_url = new_val.url
440+
else
441+
api_client.base_url = nil
442+
end
443+
end
444+
445+
state.subscribe('opencode_server', on_server_change)
446+
447+
return api_client
448+
end
413449

414450
return {
415451
new = OpencodeApiClient.new,

lua/opencode/core.lua

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
-- This file was written by an automated tool.
2-
local M = {}
32
local state = require('opencode.state')
43
local context = require('opencode.context')
54
local session = require('opencode.session')
65
local ui = require('opencode.ui.ui')
76
local server_job = require('opencode.server_job')
87
local input_window = require('opencode.ui.input_window')
98
local util = require('opencode.util')
10-
local Promise = require('opencode.promise')
119
local config = require('opencode.config')
1210

11+
local M = {}
12+
M._abort_count = 0
13+
1314
---@param parent_id string?
1415
function M.select_session(parent_id)
1516
local all_sessions = session.get_all_workspace_sessions() or {}
@@ -137,6 +138,7 @@ function M.after_run(prompt)
137138
context.unload_attachments()
138139
state.last_sent_context = vim.deepcopy(context.context)
139140
require('opencode.history').write(prompt)
141+
M._abort_count = 0
140142
end
141143

142144
---@param opts? SendMessageOpts
@@ -175,15 +177,23 @@ end
175177
function M.stop()
176178
if state.windows and state.active_session then
177179
if state.is_running() then
178-
vim.notify('Aborting current request...', vim.log.levels.WARN)
180+
M._abort_count = M._abort_count + 1
181+
state.api_client:abort_session(state.active_session.id):wait()
179182

180-
-- FIXME: I think my understanding / logic was wrong here. We don't
181-
-- just want to cancel our requests to the opencode server, we
182-
-- want the opencode server to cance it's requests. Commenting out
183-
-- this code for now and will do more testing
184-
-- server_job.cancel_all_requests()
183+
if M._abort_count >= 3 then
184+
vim.notify('Re-starting Opencode server')
185+
M._abort_count = 0
186+
-- close existing server
187+
state.opencode_server:shutdown():wait()
185188

186-
state.api_client:abort_session(state.active_session.id):wait()
189+
-- start a new one
190+
state.opencode_server = nil
191+
state.job_count = 0
192+
193+
-- NOTE: start a new server here to make sure we're subscribed
194+
-- to server events before a user sends a message
195+
state.opencode_server = server_job.ensure_server() --[[@as OpencodeServer]]
196+
end
187197
end
188198
require('opencode.ui.footer').clear()
189199
input_window.set_content('')
@@ -231,7 +241,7 @@ function M.setup()
231241
M.opencode_ok()
232242
end)
233243
local OpencodeApiClient = require('opencode.api_client')
234-
state.api_client = OpencodeApiClient.new()
244+
state.api_client = OpencodeApiClient.create()
235245
end
236246

237247
return M

0 commit comments

Comments
 (0)