[Bug]: A message sent while the socket is dead is dropped, then may reappear after resync; retry it with the same commandId instead#24659

opened17d agobyMmatthias-scale

Summary

When the WebSocket is dead or half-open, a send fails. Web and desktop then drop the message: the optimistic row is removed, and the text goes back to the composer. If the server had already saved the command before the socket died, the message comes back after the next resync. So the user sees it vanish, retypes or resends, and can end up with two copies. The server already de-duplicates on commandId, so the client could hold the send and retry it with the same commandId after reconnect.

Version / setup

  • 0.1.3-nightly.20261023.3566, macOS desktop client, remote t3 serve (Linux) over T3 Connect.
  • Same code on main at edb2b2f365.

What happens

  1. The socket goes quiet (server busy, network drop). The client only notices after the 15 s ping timeout (protocol.ts:12). Reconnect backoff then grows to 300 s (supervisor.ts:41-42).
  2. A send in this window adds an optimistic row (useComposerSend.ts:1115) and dispatches message.dispatch with a fresh commandId (commands.ts:628, :755).
  3. On socket loss the RPC fails; it is not held or re-sent. On failure the composer removes the optimistic row and restores the draft (useComposerSend.ts:1360-1384) and shows "Failed to send message."
  4. If the server committed the command before the socket died, the message reappears when the thread resyncs; we saw this 1–2 minutes later, which matches the backoff. If the user resends, the new send gets a new commandId, so the server cannot tell it is the same message.

There is no web/desktop outbox. Mobile already has one (apps/mobile/src/state/thread-outbox-*).

Server side already supports a safe retry

commitCommandEffect reserves a receipt with insertIfAbsent(commandId). If the receipt exists it returns the earlier result with committed: false (EventSink.ts:645-657). Replaying the same commandId is therefore idempotent. A new commandId with the same messageId is not covered.

Ask

  • Keep a failed send pending (shown as "waiting to send", not removed) while the environment is reconnecting.
  • After reconnect and resync, re-dispatch it with the same commandId. If the resynced thread already has the messageId, drop the pending entry.
  • Only fall back to "restore draft + error" after a terminal rejection or a user cancel.

Numbers

We have no client-side disconnect log, so we cannot give a count of affected sends. The window is at least 15 s per drop (ping timeout) and up to the backoff cap (300 s).