From fb435d225f9ab9dd3205e15d2d83f00288f61c6c Mon Sep 17 00:00:00 2001 From: Brandon Elliott <923787+Brandon168@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:29:17 -0600 Subject: [PATCH] fix(server): stop liveness pings on plugin sessions Every DaemonClient pings its daemon every 10 s to notice a silently dead socket. A plugin's session runs over its IPC channel (or an in-process queue for built-in plugins), whose loss is reported directly, and the daemon already exempts plugin sockets from the application lease those pings renew. The pings only woke both processes: about 2 wakeups a second in an idle daemon with nine plugins and the built-ins. DaemonClient gets a livenessHeartbeat option (default on); the plugin host turns it off. Explicit verification (ensureConnected({ verify: true })) still pings. Co-Authored-By: Claude Opus 5.5 --- packages/client/src/daemon-client.test.ts | 15 ++++++++++++++- packages/client/src/daemon-client.ts | 13 ++++++++++++- .../server/src/server/plugins/plugin-process.ts | 3 +++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 2ddef8cc61..c1c7821d91 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -753,13 +753,14 @@ class DaemonClientSession { private readonly daemon = new FakeDaemon(); private readonly client: DaemonClient; - constructor() { + constructor(options: { livenessHeartbeat?: boolean } = {}) { this.client = new DaemonClient({ url: "ws://test", clientId: "clsk_heartbeat_test", logger: noopLogger, reconnect: { enabled: false }, transportFactory: () => this.daemon.transport, + ...options, }); clients.push(this.client); } @@ -3257,6 +3258,18 @@ test("starts pinging one interval after connecting and holds the cadence", async expect(session.pingTimestamps()).toEqual(["10s", "20s", "30s"]); }); +test("a client with the liveness heartbeat off never pings on its own", async () => { + useHeartbeatClock(); + const session = new DaemonClientSession({ livenessHeartbeat: false }); + session.daemonAnswersPingsAfter("fast"); + + await session.connect(); + await session.advance(60_000); + + expect(session.state()).toEqual({ status: "connected" }); + expect(session.pingTimestamps()).toEqual([]); +}); + test("stops pinging once the connection is gone", async () => { useHeartbeatClock(); const session = new DaemonClientSession(); diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index a401de273f..83033282e5 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -469,6 +469,13 @@ export interface DaemonClientConfig { runtimeMetricsWindowMs?: number; trace?: DaemonClientTrace; capabilities?: Partial>; + /** + * Ping the daemon every 10 s and reconnect after missed replies (default true). Turn it off + * only where the transport reports its own loss (a plugin's IPC channel, an in-process + * channel): there a ping cannot find a dead link the channel would not, and it costs both + * processes a wakeup per client every 10 s. + */ + livenessHeartbeat?: boolean; } export interface DaemonClientTrace { @@ -2720,7 +2727,11 @@ export class DaemonClient { } private scheduleNextLivenessHeartbeat(): void { - if (this.connectionState.status !== "connected" || this.livenessHeartbeatTimer) { + if ( + this.config.livenessHeartbeat === false || + this.connectionState.status !== "connected" || + this.livenessHeartbeatTimer + ) { return; } this.livenessHeartbeatTimer = setTimeout(() => { diff --git a/packages/server/src/server/plugins/plugin-process.ts b/packages/server/src/server/plugins/plugin-process.ts index 1c0d6bc476..1ef843db83 100644 --- a/packages/server/src/server/plugins/plugin-process.ts +++ b/packages/server/src/server/plugins/plugin-process.ts @@ -313,6 +313,9 @@ export function createPluginWorker(options: { // The runtime re-attaches a session when the daemon drops this socket. reconnect: { enabled: true }, transportFactory, + // The channel is a pipe to the daemon (or an in-process queue for built-in plugins); its + // loss is reported directly, so a 10 s ping would only wake both sides. + livenessHeartbeat: false, }); paseo = createPaseoApi(daemonClient); await daemonClient.connect();