Add heartbeat event and daemon browser-liveness reaper

Introduce a `system.heartbeat` protocol event and use it as a
liveness signal on the daemon side.

- Track `last_seen` per connected browser, bumped on every inbound
  WS frame (tool response, event, ping/pong, or heartbeat).
- Add a reaper task that drops any browser silent past a 60s window
  and purges its sessions, mirroring the WS disconnect cleanup. This
  releases half-open connections left behind when the OS resumes from
  sleep and kills the service worker without delivering a close frame,
  so the daemon no longer pins a phantom "connected" browser and can
  idle-exit normally.
- Wire the reaper into both the foreground daemon and the test/embed
  entry point, aborting it on shutdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
drakezhang
2026-07-22 12:16:56 +08:00
co-authored by Cursor
parent ccf3c32140
commit 5e91178e03
8 changed files with 161 additions and 2 deletions
+5
View File
@@ -82,6 +82,7 @@ pub struct DaemonHandle {
ws: WsHandle,
ipc: Option<IpcHandle>,
session_idle_task: JoinHandle<()>,
browser_liveness_task: JoinHandle<()>,
}
impl DaemonHandle {
@@ -90,12 +91,14 @@ impl DaemonHandle {
ws: WsHandle,
ipc: Option<IpcHandle>,
session_idle_task: JoinHandle<()>,
browser_liveness_task: JoinHandle<()>,
) -> Self {
Self {
state,
ws,
ipc,
session_idle_task,
browser_liveness_task,
}
}
@@ -116,6 +119,8 @@ impl DaemonHandle {
pub async fn shutdown(self) {
self.session_idle_task.abort();
let _ = await_join(self.session_idle_task).await;
self.browser_liveness_task.abort();
let _ = await_join(self.browser_liveness_task).await;
self.ws.shutdown.notify_waiters();
let _ = await_join(self.ws.task).await;
if let Some(ipc) = self.ipc {