From 55a498603f16e8593533c944936c2eb14b3748c8 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 17 May 2026 21:55:23 +0200 Subject: [PATCH] fix(rpc): don't unref the sf-feedback drain timer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drainer was scheduled via setTimeout(0) with timer.unref(). The unref made the timer release-eligible — fine in a long-running rpc-mode child where the process has plenty of other event-loop handles, but fatal in the packaged-standalone path where the rpc subprocess has nothing else to keep it alive. The process exited before the timer fired, so the queue file was renamed to ..draining and then stranded forever. Removed timer.unref(). The setTimeout(0) still lets the RPC response go back to the caller first (no synchronous blocking on the drain), but the timer now keeps the process alive until the drain handler runs, and the drain's own async I/O keeps it alive until done. Refs sf-mpa6wuhm-wwddd1. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/coding-agent/src/modes/rpc/rpc-mode.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 0eb081d72..0d1d1e884 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -181,10 +181,17 @@ async function drainQueuedSfFeedbackCommands(cwd: string): Promise { } function scheduleQueuedSfFeedbackDrain(cwd: string): void { - const timer = setTimeout(() => { + // Intentionally NOT unref'd: the drain must keep the process alive until it + // finishes. In a long-running rpc-mode child this is moot — the process has + // plenty of other handles. In a transient drainer subprocess (packaged- + // standalone path) the setTimeout was the only event-loop handle, so an + // unref'd timer let Node exit before the drain ran, stranding the renamed + // .draining file forever (sf-mpa6wuhm-wwddd1). The setTimeout(…, 0) is + // still desirable so the RPC response goes back to the caller first; we + // just don't want the timer to be release-eligible. + setTimeout(() => { void drainQueuedSfFeedbackCommands(cwd); }, 0); - timer.unref?.(); } async function captureProcessWrites(