@@ -21,6 +21,7 @@ local SERVICE_OUTPUT_HISTORY_LIMIT = 20 -- Keep only the most recent startup lin
2121local CONTROL_REQUEST_TIMEOUT_SECONDS = ' 2' -- Bound local control-socket curl calls so stale sockets cannot hang startup or exit indefinitely.
2222local SAVED_ADDR_PROBE_TIMEOUT_SECONDS = ' 1' -- Quick probe timeout when validating persisted service addresses.
2323local STALE_ADDR_FAILURE_THRESHOLD = 3 -- After this many consecutive health-check failures on the same discovered address, force a fresh restart.
24+ local DEFAULT_CLIENT_HEARTBEAT_INTERVAL_MS = 10000 -- Refresh client registration periodically so detached idle shutdown never races active chat use.
2425local DEFAULT_SERVICE_LOG_PATH = vim .fn .stdpath (' state' ) .. ' /copilot-agent-service.log'
2526local DEFAULT_CLIENT_ID_LENGTH = 32
2627
@@ -648,6 +649,63 @@ local function interval_settings()
648649 return timeout_ms , interval_ms
649650end
650651
652+ local function client_heartbeat_interval_ms ()
653+ local interval = tonumber (state .config .service .client_heartbeat_interval_ms )
654+ if interval == nil then
655+ interval = DEFAULT_CLIENT_HEARTBEAT_INTERVAL_MS
656+ end
657+ return math.max (0 , interval )
658+ end
659+
660+ local function close_client_heartbeat_timer ()
661+ local timer = state .client_heartbeat_timer
662+ if timer == nil then
663+ return
664+ end
665+ pcall (function ()
666+ timer :stop ()
667+ timer :close ()
668+ end )
669+ state .client_heartbeat_timer = nil
670+ end
671+
672+ local function start_client_heartbeat_timer ()
673+ if state .shutting_down then
674+ return
675+ end
676+
677+ local interval_ms = client_heartbeat_interval_ms ()
678+ if interval_ms <= 0 then
679+ return
680+ end
681+
682+ if state .client_heartbeat_timer == nil then
683+ state .client_heartbeat_timer = uv .new_timer ()
684+ end
685+ local timer = state .client_heartbeat_timer
686+ if timer == nil then
687+ return
688+ end
689+
690+ timer :stop ()
691+ timer :start (
692+ interval_ms ,
693+ interval_ms ,
694+ vim .schedule_wrap (function ()
695+ if state .shutting_down then
696+ return
697+ end
698+ if type (state .client_registered_base_url ) ~= ' string' or state .client_registered_base_url == ' ' then
699+ return
700+ end
701+ local registered , register_err = M .register_client ()
702+ if not registered and register_err then
703+ log (' client heartbeat renewal failed: ' .. tostring (register_err ), vim .log .levels .DEBUG )
704+ end
705+ end )
706+ )
707+ end
708+
651709write_text_file = function (path , content )
652710 local ok , err = pcall (function ()
653711 local f = assert (io.open (path , ' w' ))
@@ -958,6 +1016,7 @@ function M.register_client()
9581016
9591017 if state .client_registered_base_url == base_url then
9601018 log (string.format (' client already registered client_id=%s base_url=%s' , M .client_id (), base_url ), vim .log .levels .DEBUG )
1019+ start_client_heartbeat_timer ()
9611020 return true
9621021 end
9631022
@@ -990,11 +1049,13 @@ function M.register_client()
9901049 end
9911050
9921051 state .client_registered_base_url = base_url
1052+ start_client_heartbeat_timer ()
9931053 log (string.format (' registering client succeeded client_id=%s base_url=%s' , M .client_id (), base_url ), vim .log .levels .DEBUG )
9941054 return true
9951055end
9961056
9971057function M .unregister_client ()
1058+ close_client_heartbeat_timer ()
9981059 local base_url = type (state .client_registered_base_url ) == ' string' and state .client_registered_base_url or current_service_base_url ()
9991060 if base_url == ' ' then
10001061 log (' unregister_client skipped: no registered base_url' , vim .log .levels .DEBUG )
0 commit comments