From 88dcd7b775fca0402b89d715493fcf4cede72d59 Mon Sep 17 00:00:00 2001 From: Julian Brown Date: Wed, 8 Oct 2025 14:22:43 -0400 Subject: [PATCH 1/2] fix: detect system sleep and prevent false keepalive timeouts When laptop wakes from sleep, the ping timer's elapsed time exceeds the normal interval. This triggers immediate timeout checks on all clients that haven't responded during the sleep period, causing false disconnections. Reset all client pong timestamps after detecting wake events (>1.5x interval elapsed) to give clients a fresh keepalive window. --- lua/claudecode/server/tcp.lua | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/lua/claudecode/server/tcp.lua b/lua/claudecode/server/tcp.lua index 273adff..8afb400 100644 --- a/lua/claudecode/server/tcp.lua +++ b/lua/claudecode/server/tcp.lua @@ -247,6 +247,7 @@ end ---@return table? timer The timer handle, or nil if creation failed function M.start_ping_timer(server, interval) interval = interval or 30000 -- 30 seconds + local last_run = vim.loop.now() local timer = vim.loop.new_timer() if not timer then @@ -255,19 +256,46 @@ function M.start_ping_timer(server, interval) end timer:start(interval, interval, function() + local now = vim.loop.now() + local elapsed = now - last_run + + -- Detect potential system sleep: timer interval was significantly exceeded + -- Allow 50% grace period (e.g., 45s instead of 30s) to account for system load + local is_wake_from_sleep = elapsed > (interval * 1.5) + + if is_wake_from_sleep then + -- After system sleep/wake, reset all client pong timestamps to prevent false timeouts + -- This gives clients a fresh keepalive window since the time jump isn't their fault + require("claudecode.logger").debug( + "server", + string.format("Detected potential wake from sleep (%.1fs elapsed), resetting client keepalive timers", elapsed / 1000) + ) + for _, client in pairs(server.clients) do + if client.state == "connected" then + client.last_pong = now + end + end + end + for _, client in pairs(server.clients) do if client.state == "connected" then - -- Check if client is alive + -- Check if client is alive (local connections, so use standard timeout) if client_manager.is_client_alive(client, interval * 2) then client_manager.send_ping(client, "ping") else - -- Client appears dead, close it - server.on_error("Client " .. client.id .. " appears dead, closing") + -- Client connection timed out - log at INFO level (this is expected behavior) + local time_since_pong = math.floor((now - client.last_pong) / 1000) + require("claudecode.logger").info( + "server", + string.format("Client %s keepalive timeout (%ds idle), closing connection", client.id, time_since_pong) + ) client_manager.close_client(client, 1006, "Connection timeout") M._remove_client(server, client) end end end + + last_run = now end) return timer From 0bbe0007d2fdd0e9169075a723096e7a9132ed3f Mon Sep 17 00:00:00 2001 From: Julian Brown Date: Thu, 16 Oct 2025 11:25:02 -0400 Subject: [PATCH 2/2] run stylua --- lua/claudecode/server/tcp.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/claudecode/server/tcp.lua b/lua/claudecode/server/tcp.lua index 8afb400..297aa20 100644 --- a/lua/claudecode/server/tcp.lua +++ b/lua/claudecode/server/tcp.lua @@ -268,7 +268,10 @@ function M.start_ping_timer(server, interval) -- This gives clients a fresh keepalive window since the time jump isn't their fault require("claudecode.logger").debug( "server", - string.format("Detected potential wake from sleep (%.1fs elapsed), resetting client keepalive timers", elapsed / 1000) + string.format( + "Detected potential wake from sleep (%.1fs elapsed), resetting client keepalive timers", + elapsed / 1000 + ) ) for _, client in pairs(server.clients) do if client.state == "connected" then