Gate liveness reaping on heartbeat capability

Only reap browsers that have sent at least one `system.heartbeat`.
A pre-heartbeat extension never opts in, so it is never dropped on
silence and keeps relying on socket-close detection exactly as before.
This removes a backward-compat regression where a new daemon paired
with an old extension would wrongly reap a live-but-idle connection
and then report `no_browser_connected` on the next command.

- Add a `heartbeat_seen` flag to `BrowserClient`, set when a heartbeat
  event arrives, and exclude never-heartbeated clients from
  `stale_browsers`.
- Cover both the opt-in and the legacy-survivor cases in unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
drakezhang
2026-07-22 13:06:17 +08:00
co-authored by Cursor
parent ed92e54dff
commit 31d2a1b3dc
4 changed files with 55 additions and 9 deletions
+5 -2
View File
@@ -305,6 +305,7 @@ async fn drive_connection(
connected_at_ms,
version_skew,
last_seen: std::sync::Mutex::new(std::time::Instant::now()),
heartbeat_seen: std::sync::atomic::AtomicBool::new(false),
});
state.browsers.insert(Arc::clone(&client));
info!(
@@ -424,8 +425,10 @@ async fn handle_inbound_text(state: &Arc<DaemonState>, client: &Arc<BrowserClien
}
Frame::Event(ev) => match ev.event {
bsk_protocol::EventKind::SystemHeartbeat => {
// Liveness only — `touch()` already ran for this frame in
// the read loop, so there is nothing else to do.
// `touch()` already ran for this frame in the read loop;
// additionally opt this browser in to liveness reaping now
// that we know it speaks the heartbeat.
client.mark_heartbeat_seen();
debug!(id = %client.id, "heartbeat");
}
bsk_protocol::EventKind::SessionWindowClosed => {