Skip to content

Commit 0de96a8

Browse files
authored
fix: detect system sleep and prevent false keepalive timeouts (#141)
* 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. * run stylua
1 parent 6af7df0 commit 0de96a8

File tree

1 file changed

+34
-3
lines changed

1 file changed

+34
-3
lines changed

lua/claudecode/server/tcp.lua

Lines changed: 34 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,49 @@ 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(
272+
"Detected potential wake from sleep (%.1fs elapsed), resetting client keepalive timers",
273+
elapsed / 1000
274+
)
275+
)
276+
for _, client in pairs(server.clients) do
277+
if client.state == "connected" then
278+
client.last_pong = now
279+
end
280+
end
281+
end
282+
258283
for _, client in pairs(server.clients) do
259284
if client.state == "connected" then
260-
-- Check if client is alive
285+
-- Check if client is alive (local connections, so use standard timeout)
261286
if client_manager.is_client_alive(client, interval * 2) then
262287
client_manager.send_ping(client, "ping")
263288
else
264-
-- Client appears dead, close it
265-
server.on_error("Client " .. client.id .. " appears dead, closing")
289+
-- Client connection timed out - log at INFO level (this is expected behavior)
290+
local time_since_pong = math.floor((now - client.last_pong) / 1000)
291+
require("claudecode.logger").info(
292+
"server",
293+
string.format("Client %s keepalive timeout (%ds idle), closing connection", client.id, time_since_pong)
294+
)
266295
client_manager.close_client(client, 1006, "Connection timeout")
267296
M._remove_client(server, client)
268297
end
269298
end
270299
end
300+
301+
last_run = now
271302
end)
272303

273304
return timer

0 commit comments

Comments
 (0)