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 <noreply@anthropic.com>
This commit is contained in:
Brandon Elliott
2026-11-09 23:36:48 -06:00
co-authored by Claude Opus 5.5
parent d46ee6558c
commit fb435d225f
3 changed files with 29 additions and 2 deletions
+14 -1
View File
@@ -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();
+12 -1
View File
@@ -469,6 +469,13 @@ export interface DaemonClientConfig {
runtimeMetricsWindowMs?: number;
trace?: DaemonClientTrace;
capabilities?: Partial<Record<ClientCapability, unknown>>;
/**
* 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(() => {
@@ -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();