From e0c910cf0928c5f8f305f5d51ad0c89fe6c1e9a0 Mon Sep 17 00:00:00 2001 From: phanium <91544758+phanen@users.noreply.github.com> Date: Sun, 28 Jun 2026 13:58:23 +0800 Subject: [PATCH] fix(cancel): always call abort_session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: `M.cancel` was gated on `state.active_session and state.jobs.is_running()`. `state.jobs.is_running()` is true only while the client has an in-flight HTTP request; once the request returns, the counter is back to zero. So attaching to a session whose model is already processing, or toggling off/on with `persist_state`, leaves cancel with nothing to do. The counter that drives the 3-strike server restart must keep gating on `state.jobs.is_running()` — its purpose is to detect the user is trying to abort an in-flight request and the server is not responding, not to count cancel attempts on an idle session. Solution: Move the `state.jobs.is_running()` gate to wrap only the counter increment. The cancel itself always runs. `abort_session` is a no-op on the server for an idle session, so calling it is harmless. Tests: - `aborts running session even when ui is not visible` (existing) covers the in-flight path. - `aborts when the model is processing on the server but no client request is in flight` reproduces the bug and asserts abort is called even with no in-flight request. - `does not count cancel toward the server-restart threshold when no client request is in flight` confirms the counter gate is preserved, so a stuck-idle path does not trigger a spurious server restart. --- lua/opencode/services/session_runtime.lua | 6 ++-- tests/unit/services_session_runtime_spec.lua | 32 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lua/opencode/services/session_runtime.lua b/lua/opencode/services/session_runtime.lua index 8c439100..9fa3948b 100644 --- a/lua/opencode/services/session_runtime.lua +++ b/lua/opencode/services/session_runtime.lua @@ -241,8 +241,10 @@ end ---@param opts? SendMessageOpts M.cancel = Promise.async(function() - if state.active_session and state.jobs.is_running() then - vim.g.opencode_abort_count = (vim.g.opencode_abort_count or 0) + 1 + if state.active_session then + if state.jobs.is_running() then + vim.g.opencode_abort_count = (vim.g.opencode_abort_count or 0) + 1 + end local permissions = state.pending_permissions or {} if #permissions > 0 and state.api_client then diff --git a/tests/unit/services_session_runtime_spec.lua b/tests/unit/services_session_runtime_spec.lua index 663156f7..11f3aa33 100644 --- a/tests/unit/services_session_runtime_spec.lua +++ b/tests/unit/services_session_runtime_spec.lua @@ -689,6 +689,38 @@ describe('opencode.services.session_runtime', function() abort_stub:revert() end) + + it('aborts when the model is processing on the server but no client request is in flight', function() + state.session.set_active({ id = 'sess1' }) + store.set('job_count', 0) + + local abort_stub = stub(state.api_client, 'abort_session').invokes(function() + return Promise.new():resolve(true) + end) + + session_runtime.cancel():wait() + + assert.stub(abort_stub).was_called() + + abort_stub:revert() + end) + + it('does not count cancel toward the server-restart threshold when no client request is in flight', function() + state.session.set_active({ id = 'sess1' }) + store.set('job_count', 0) + vim.g.opencode_abort_count = 0 + + for _ = 1, 5 do + session_runtime.cancel():wait() + end + + assert.is_equal(0, vim.g.opencode_abort_count) + + store.set('job_count', 1) + vim.g.opencode_abort_count = 0 + session_runtime.cancel():wait() + assert.is_equal(1, vim.g.opencode_abort_count) + end) end) describe('opencode_ok (version checks)', function()