fix(rpc): don't unref the sf-feedback drain timer

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 .<pid>.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) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-17 21:55:23 +02:00
parent cc67970fa0
commit 55a498603f

View file

@ -181,10 +181,17 @@ async function drainQueuedSfFeedbackCommands(cwd: string): Promise<void> {
}
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<T>(