fix(composer): self-heal a stuck "Sending..." optimistic placeholder (LC-382) #390

Merged
longjacksonle merged 1 commit from fix/lc-382-sending-stuck-watchdog into main 2026-06-19 18:57:49 +02:00

Closes LC-382.

Symptom

After sending a message the row greys out and stays on "Sending..." (the LC-230 optimistic placeholder); it only renders after a manual full refresh. The message is persisted - the POST succeeded - so only the live render is lost.

Root cause

The optimistic-echo reconcile has a single point of failure: the one WS NewMessage broadcast that carries data-lc-client-id. live.js (htmx:oobBeforeSwap) removes the matching placeholder and htmx appends the canonical row. If that single frame is missed - flaky socket, a reconnect gap between the POST landing and the socket re-subscribing, a dropped/unprocessed OOB swap - the placeholder is never removed or replaced and sits on "Sending..." forever, with no fallback.

I traced the whole chain (placeholder builder, server finalize_message_send broadcast, ws.rs::render_new_message author echo, the live.js reconcile, htmx 2.0.4, room re-subscription on htmx:wsOpen) - every piece is individually correct. The defect is the missing fallback, not any one step. (The running dev build is pre-LC-376, so this is unrelated to the room-view epic.)

Fix

A WS-miss safety net. On a successful send the composer schedules a ~4s watchdog for the sent client_id. __lcEchoReconcile checks whether the placeholder is still pending (findPending) and, if so, reloads the #messages region from the current URL via htmx.ajax(GET, location, {select:'#messages', swap:'outerHTML'}) - the manual refresh the user would do, automated - so the real message renders and the stale placeholder is dropped.

No effect on:

  • the happy path (placeholder already removed by the WS render well within the window),
  • quarantine drops (__lcEchoDrop already removed it),
  • failures (__lcEchoFailed cleared data-lc-pending),

since each leaves findPending() returning null.

Testing

  • ./dev/cargo check, just test, just test-saas - all green. Markup/JS only; server broadcast / reconcile / failure paths unchanged.

🤖 Generated with Claude Code

Closes LC-382. ## Symptom After sending a message the row greys out and stays on "Sending..." (the LC-230 optimistic placeholder); it only renders after a manual full refresh. The message *is* persisted - the POST succeeded - so only the live render is lost. ## Root cause The optimistic-echo reconcile has a single point of failure: the one WS `NewMessage` broadcast that carries `data-lc-client-id`. `live.js` (`htmx:oobBeforeSwap`) removes the matching placeholder and htmx appends the canonical row. If that single frame is missed - flaky socket, a reconnect gap between the POST landing and the socket re-subscribing, a dropped/unprocessed OOB swap - the placeholder is never removed or replaced and sits on "Sending..." forever, with no fallback. I traced the whole chain (placeholder builder, server `finalize_message_send` broadcast, `ws.rs::render_new_message` author echo, the `live.js` reconcile, htmx 2.0.4, room re-subscription on `htmx:wsOpen`) - every piece is individually correct. The defect is the missing fallback, not any one step. (The running dev build is pre-LC-376, so this is unrelated to the room-view epic.) ## Fix A WS-miss safety net. On a successful send the composer schedules a ~4s watchdog for the sent `client_id`. `__lcEchoReconcile` checks whether the placeholder is still pending (`findPending`) and, if so, reloads the `#messages` region from the current URL via `htmx.ajax(GET, location, {select:'#messages', swap:'outerHTML'})` - the manual refresh the user would do, automated - so the real message renders and the stale placeholder is dropped. No effect on: - the happy path (placeholder already removed by the WS render well within the window), - quarantine drops (`__lcEchoDrop` already removed it), - failures (`__lcEchoFailed` cleared `data-lc-pending`), since each leaves `findPending()` returning null. ## Testing - `./dev/cargo check`, `just test`, `just test-saas` - all green. Markup/JS only; server broadcast / reconcile / failure paths unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(composer): self-heal a stuck "Sending..." optimistic placeholder (LC-382)
All checks were successful
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 6s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 2m48s
af38a89f67
The LC-230 optimistic echo renders a pending placeholder on send and relies on a single WS NewMessage broadcast (carrying data-lc-client-id) to reconcile it: live.js removes the placeholder and htmx appends the canonical row. If that one broadcast is missed - a flaky socket, a reconnect gap between the POST landing and the socket re-subscribing, a dropped/unprocessed OOB swap - the placeholder is never removed or replaced and sits on "Sending..." forever; the message is persisted (a manual refresh renders it) but the live render is lost.

Add a WS-miss safety net. On a successful send the composer now schedules a ~4s watchdog for the sent client_id; __lcEchoReconcile checks whether the placeholder is still pending (findPending) and, if so, reloads the #messages region from the current URL via htmx.ajax(select:'#messages', swap:'outerHTML') - the manual refresh the user would otherwise do, automated - so the real message renders and the stale placeholder is dropped.

No effect on the happy path (the placeholder is already gone well within the window), on quarantine drops (__lcEchoDrop removed it), or on failures (__lcEchoFailed cleared data-lc-pending), since each leaves findPending() returning null. Markup/JS only; the server broadcast, reconcile, and failure paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch fix/lc-382-sending-stuck-watchdog 2026-06-19 18:57:49 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!390
No description provided.