Skip to content

Commit 88dcd7b

Browse files
committed
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.
1 parent ac2baef commit 88dcd7b

File tree

1 file changed

+31
-3
lines changed

1 file changed

+31
-3
lines changed

lua/claudecode/server/tcp.lua

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ end
247247
---@return table? timer The timer handle, or nil if creation failed
248248
function M.start_ping_timer(server, interval)
249249
interval = interval or 30000 -- 30 seconds
250+
local last_run = vim.loop.now()
250251

251252
local timer = vim.loop.new_timer()
252253
if not timer then
@@ -255,19 +256,46 @@ function M.start_ping_timer(server, interval)
255256
end
256257

257258
timer:start(interval, interval, function()
259+
local now = vim.loop.now()
260+
local elapsed = now - last_run
261+
262+
-- Detect potential system sleep: timer interval was significantly exceeded
263+
-- Allow 50% grace period (e.g., 45s instead of 30s) to account for system load
264+
local is_wake_from_sleep = elapsed > (interval * 1.5)
265+
266+
if is_wake_from_sleep then
267+
-- After system sleep/wake, reset all client pong timestamps to prevent false timeouts
268+
-- This gives clients a fresh keepalive window since the time jump isn't their fault
269+
require("claudecode.logger").debug(
270+
"server",
271+
string.format("Detected potential wake from sleep (%.1fs elapsed), resetting client keepalive timers", elapsed / 1000)
272+
)
273+
for _, client in pairs(server.clients) do
274+
if client.state == "connected" then
275+
client.last_pong = now
276+
end
277+
end
278+
end
279+
258280
for _, client in pairs(server.clients) do
259281
if client.state == "connected" then
260-
-- Check if client is alive
282+
-- Check if client is alive (local connections, so use standard timeout)
261283
if client_manager.is_client_alive(client, interval * 2) then
262284
client_manager.send_ping(client, "ping")
263285
else
264-
-- Client appears dead, close it
265-
server.on_error("Client " .. client.id .. " appears dead, closing")
286+
-- Client connection timed out - log at INFO level (this is expected behavior)
287+
local time_since_pong = math.floor((now - client.last_pong) / 1000)
288+
require("claudecode.logger").info(
289+
"server",
290+
string.format("Client %s keepalive timeout (%ds idle), closing connection", client.id, time_since_pong)
291+
)
266292
client_manager.close_client(client, 1006, "Connection timeout")
267293
M._remove_client(server, client)
268294
end
269295
end
270296
end
297+
298+
last_run = now
271299
end)
272300

273301
return timer

0 commit comments

Comments
 (0)