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();