fix: tps mail send no longer hangs after delivery#251
Conversation
Three causes of the exit hang: 1. relay.ts deliverToRemoteBranch: after ACK, held a 2s drain window to receive inbound messages. This is sync work, not send work — removed entirely. Inbound sync belongs in connectAndKeepAlive / tps mail sync. 2. ws-noise-transport.ts WsNoiseChannel.close(): called ws.close() fire-and-forget. WS close handshake kept event loop alive. Now awaits the 'close' event with a 2s unref() safety timeout so it never hangs indefinitely. 3. noise-ik-transport.ts NoiseIkChannel.close(): same issue — socket.end() fire-and-forget. Now awaits 'close' event with 2s unref() safety timeout. Result: tps mail send exits immediately after delivery ACK.
tps-kern
left a comment
There was a problem hiding this comment.
Architecture verified. Removing the 2-second arbitrary drain window from the one-shot send path is absolutely the right call—message sync is the responsibility of the keepalive daemon, not a side-effect of a send command. Adding graceful closures to the transports with a 2s unref() timeout ensures we don't leak sockets while guaranteeing the CLI process can exit cleanly even if the connection stalls. Approved.
tps-sherlock
left a comment
There was a problem hiding this comment.
Security review complete. No security implications found.
The timeout additions with .unref() are the correct way to guarantee that a stalled close() sequence does not hang the event loop and prevent Node from exiting.
The removal of the 2-second drain window on the send side avoids unnecessary blocking.
Approved.
Problem
tps mail sendto a remote branch sent successfully but never exited, blocking scripting and automation.Root Causes (3)
1. relay.ts — spurious 2s drain window
After receiving the delivery ACK,
deliverToRemoteBranchheld the connection open for 2s to drain inbound messages. This is sync work, not send work — it doesn't belong here.2. ws-noise-transport.ts — WS close not awaited
WsNoiseChannel.close()calledws.close()fire-and-forget. The WS close handshake kept the event loop alive indefinitely.3. noise-ik-transport.ts — TCP socket close not awaited
Same pattern —
socket.end()fire-and-forget left the socket in the event loop.Fix
deliverToRemoteBranch— close immediately after ACKWsNoiseChannel.close(): await'close'event with 2sunref()safety timeoutNoiseIkChannel.close(): await'close'event with 2sunref()safety timeoutThe
unref()on the safety timers means they don't prevent exit on their own — they're purely a guard against a stalled close handshake.693 passing, 4 pre-existing failures.