Repository navigation
[Bug]: Durable SSH terminal remains falsely Attached and non-interactive after RPC stream timeout #3439
Description
Current Behavior
A durable SSH terminal intermittently becomes completely non-interactive:
- Terminal output stops updating.
- Keyboard input has no effect.
- The shield continues to show Durable Session (Attached).
- Wave reports the SSH connection as connected.
- The remote shell and foreground application remain healthy.
- Fully quitting and reopening Wave successfully reattaches to the original session and restores interactivity.
In the captured occurrence, a Codex process was running in the terminal. After the Wave block stopped responding, Codex continued working remotely and completed its task. The original processes remained alive:
PID 1964 wsh jobmanager
PID 1973 bash
PID 3100 codex
After restarting Wave, the same PIDs were preserved and the existing session became responsive again.
The remote durable-session log recorded:
2026/07/20 16:09:19 SendData: error sending stream data: timeout sending request
Wave did not mark the job disconnected after this error. The shield remained Attached and automatic reconnection did not occur.
When Wave was restarted, the remote log showed:
2026/07/20 18:57:45 AuthenticateToJobManager: authentication successful
2026/07/20 18:57:45 SetAttachedClient: kicking out existing client
2026/07/20 18:57:45 connectToStreamHelper: disconnecting existing client
2026/07/20 18:57:45 connectToStreamHelper: detached old stream id=cf519bf0-ae5e-4a93-9119-53c63534e019
2026/07/20 18:57:45 PrepareConnect: streamid=b21b548f-887d-4e49-8d3d-025521da369c clientSeq=19863439 serverSeq=19863439 streamDone=false streamError="" hasExited=false
2026/07/20 18:57:45 StartStream: streamid=b21b548f-887d-4e49-8d3d-025521da369c rwnd=65536 streaming started
clientSeq and serverSeq were identical and totalGap=0 in the local Wave log, indicating the backend had the complete terminal stream even though the terminal block was frozen.
There was an earlier local WebSocket timeout for the exact tab and block:
2026-07-20 15:35:21.507 [websocket] WritePump error
write tcp 127.0.0.1:45359->127.0.0.1:33439: i/o timeout
wshrouter unbind route "feblock:777680bf-221f-4bba-9cb3-5689b245b1f3"
wshrouter unregister link 6#[ws:tab:9c4d3ecd-3fda-4dd1-99bc-dc1aa0879e74]
Wave immediately created another WebSocket link and rebound the block, but the terminal later became unresponsive.
Similar remote SendData: timeout sending request errors occurred on July 15, July 19, and twice on July 20 across durable sessions.
Expected Behavior
When the durable job's RPC or stream path stops accepting data:
- Wave should mark the durable job Detached or Stalled.
- The shield should not continue showing Attached.
- Wave should close the stale route and automatically reconnect to the existing job manager.
- Output and input health should be checked end-to-end rather than relying only on route registration.
- Recovery should not require restarting the entire Wave application.
Steps To Reproduce
The failure is intermittent:
- Run Wave v0.14.5 on Windows.
- Open an SSH terminal to a Linux host.
- Enable Durable Session.
- Run a long-lived interactive TUI that produces sustained output, such as Codex CLI.
- Leave the session running for several hours.
- Eventually, the terminal may stop updating and accepting input.
- Observe that the shield still says Durable Session (Attached) and the SSH connection still says connected.
- Verify from another SSH session that the remote shell/application remains alive.
- Fully quit and reopen Wave.
- Observe that Wave reattaches to the original shell and application and the terminal becomes responsive.
Environment details
Wave Version: v0.14.5 (build 202604161539), Electron 41.1.0, remote wsh v0.14.5
Platform: Windows x64
Remote SSH host: Linux 6.8.0-136-generic x86_64
Additional investigation
The v0.14.5 source appears to explain why the state remains falsely Attached.
routedDataSender.SendData logs and ignores errors.
The RPC enqueue times out after five seconds.
The frontend displays Attached solely when the stored job status is connected.
The observed sequence suggests:
- The job manager's bounded outbound channel or downstream Unix-socket writer stopped draining.
SendDatatimed out.- The error was logged but did not close the stale connection.
- No route-down event was generated.
- The job status remained
connected, leaving the shield falsely Attached. - Restarting Wave rebuilt the router and reconnected the original job successfully.
There is also a related recovery problem: the documentation recommends Advanced -> Force Restart Controller for a session that will not reconnect, but in v0.14.5 that action destroys the controller. Destroying a durable controller calls TerminateAndDetachJob.
In practice, Force Restart Controller terminates the preserved session and opens a new bash shell. This should either reconnect non-destructively or be labeled as terminating the existing session.
Full logs are available, but the raw waveapp.log contains Wave routing tokens and would need to be redacted before attachment.
Activity
CitrusYL commentedon Jul 22, 2026
Same issue on Wave v0.14.5, macOS arm64. Durable SSH + tmux + codex becomes non-interactive, and Force Restart Controller restores it.
YVin3D commentedon Aug 28, 2026
fwiw it's not codex only. i get the same issue (Wave v0.14.5, macOS arm64) in codex, claude, and other durable SSH terminal blocks.
tail -fon remote logs that are rapidly outputting lines seems to trigger it a lot more consistently if anyone wants to try and replicate it. (helps if your remote server's network connection is bad)the one block will freeze while other blocks on the same SSH connection and/or the same tab are just fine.
remote process stays alive, and the stalled job-manager log prints:
Reattaching that job’s stream restores it. Seems to be from Wave’s per-job durable stream/RPC
kieled commentedon Oct 13, 2026
Also hitting this on v0.14.5 (macOS arm64 → Linux x64 over Tailscale, fish,
term:durable: true), several times a day. Each frozen job's remote log has exactly oneSendData: error sending stream data: timeout sending requestand nothing after it, while the SSH connection stays healthy (no stalled/disconnected events).I traced it in the source, and I think the issue is slightly more specific than "the error is swallowed": a single failed send permanently deadlocks the stream, because a dropped packet is never retransmitted and the receiver never ACKs past the gap. (Links are to current
main; these files are unchanged since v0.14.5.)prepareNextPacketadvancessentNotAckedbefore the send happens (streammanager.go#L409-L410).SendComplexRequestblocks for more thanDefaultTimeoutMs(5s), the packet is dropped (wshrpc.go#L802-L808).routedDataSender.SendDataonly logs this (mainserverconn.go#L44-L51), sosenderLoopcarries on as if the bytes had been delivered.RecvAckonly advances on cumulative ACKs.streamclient.Reader.RecvDataparks every later packet (Seq > nextSeq) inoooPacketsand returns without sending an ACK (streamreader.go#L71-L74).nextSeqcan never move past the lost packet.sentNotAckedreaches the 64 KB window, then waits ondrainCondforever (streammanager.go#L386-L390). The job route is still up, sohandleRouteDownEvent/ auto-reconnect never fire, and the shield keeps saying Attached.This fits the reports here and in #2985: only the affected block freezes, other blocks on the same connection keep working, the remote process keeps running, and a full reattach (app restart, or Disconnect → Reconnect) restores everything with no data loss, since
ClientDisconnected()resetssentNotAckedandPrepareConnectresumes from the client'snextSeq. Bursty TUI output (Claude Code, Codex,tail -f) makes a 5s enqueue stall more likely, which would explain why those workloads trigger it.Also worth noting for anyone who lands here: Advanced → Force Restart Controller does not reattach. It terminates the job manager and starts a new shell (my log shows
terminating job managerfollowed byRemoteStartJobCommand), so the session is lost. Use Disconnect → Reconnect instead.On #3460: I think its job-manager half (return the error from
SendData, thenClientDisconnected()+ close the stale client so the route drops and the existing auto-reconnect runs) is enough to fix this on its own. I'm less sure about the 45s output watchdog injobcontroller. A shell idle at a prompt produces no output, so it would get a disconnect/reattach cycle every ~45s. Also,runOutputLoopbreaks and closes the reader beforehandleStalledStreamdecides whether to act. When that returns early (cooldown, connection reported down, the remote-disconnect RPC timing out), nothing restarts the output loop, and the block freezes again with no recovery. Raising the timeout (as #3551 does) makes drops rarer but doesn't remove the deadlock.I just opened #3553 with just the job-manager-side fix: on a send error the stream drops back to buffering mode (only if the failing sender is still the current one) and the stale connection is closed, so the existing route-down auto-reconnect reattaches from the client's last received seq. It includes
streammanager_test.gocases with a failingDataSender, which fail onmain. On the main-server side it only changes the auto-reconnect cooldown, so a second failure inside it is deferred instead of dropped. There's no new watchdog.