Skip to content

fix: confirm server generation state before flushing queued chat messages#1387

Merged
gsxdsm merged 4 commits into
mainfrom
gsxdsm/queued-messages
Jun 4, 2026
Merged

fix: confirm server generation state before flushing queued chat messages#1387
gsxdsm merged 4 commits into
mainfrom
gsxdsm/queued-messages

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Queuing a follow-up while the assistant was still responding, hitting back, and re-entering the chat made the queued message vanish — not sent, not in the composer — and could kill the in-flight reply too. Both prior attempts (FN-5852, FN-5921) fixed the persistence of the queued draft but left the restore path trusting the client's local isGenerating flag, which is reliably stale mid-generation: it's a route-level enrichment that the chat:session:updated SSE payload lacks, so the client believes nothing is generating, flushes immediately, deletes the persisted copy, and fires a send whose beginGeneration aborts the live generation server-side.

The restore path in useChat and useQuickChat now confirms with the server (fetchChatSession) before flushing: still generating → re-attach to the stream and let onDone/onError deliver the queued message; idle → flush immediately; fetch failed → keep the restored bubble for a later flush trigger.

Regression tests in both hook suites reproduce the production state the previous tests never modeled — a stale falsy isGenerating in local state while the server reports an active generation — and assert no premature send, intact localStorage, and a correct flush once the generation completes.

Fixes #1279 (FN-5852)


Compound Engineering
Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevent queued chat messages from being lost after back-navigation; ensure queued follow-ups wait for server-confirmed generation, re-attach to in-flight responses when needed, and flush only when safe. Retain queued bubbles if server check fails.
  • Documentation

    • New troubleshooting page on queued-message flush issues and prevention guidance.
    • Expanded Chat glossary with generation, queued-message, and enrichment-field semantics.
  • Tests

    • Added regression tests verifying safe restoration and delivery behavior for queued messages.

…chat messages

Re-entering a chat flushed the restored queued message based on the
client's stale isGenerating flag (a route-level enrichment the
chat:session:updated SSE payload lacks), firing a send that aborted the
live generation server-side and could lose the message entirely. The
restore path in useChat and useQuickChat now asks the server first:
attach and defer the flush while generating, send immediately only when
no generation is in flight, and keep the bubble on a failed check.

Fixes #1279

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bb38ab0e-be69-420e-aaec-f899c12f39d9

📥 Commits

Reviewing files that changed from the base of the PR and between 47bd46e and 9239e52.

📒 Files selected for processing (1)
  • packages/dashboard/app/hooks/__tests__/useChat.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/dashboard/app/hooks/tests/useChat.test.ts

📝 Walkthrough

Walkthrough

This PR fixes queued chat message loss after back-navigation by replacing stale client-side isGenerating checks with authoritative server confirmation. Both Chat and Quick Chat hooks now fetch the current session state before flushing: if generation is active, they attach to the stream and defer flushing to completion; otherwise they flush immediately.

Changes

Queued Message Restoration Fix

Layer / File(s) Summary
Problem documentation and semantic concepts
.changeset/fn-5852-queued-message-stale-flush.md, CONCEPTS.md, docs/solutions/logic-errors/queued-chat-message-flush-trusts-stale-isgenerating.md
Changeset describes the behavioral fix. CONCEPTS.md adds glossary definitions for Generation, Queued message, and Enrichment field to establish server-owned semantics. Solution article documents the stale isGenerating root cause, SSE-driven state degradation, and the server-confirmation strategy before irreversible actions.
useChat restoration with server validation
packages/dashboard/app/hooks/useChat.ts, packages/dashboard/app/hooks/__tests__/useChat.test.ts
Hook's restore effect replaces local-state-only flush with async fetchChatSession confirmation: if server reports active generation, attaches to the stream and defers flushing to completion; otherwise flushes immediately. Cancellation guard prevents stale async results. Regression tests verify no premature flush when server confirms in-flight generation and during pending-fetch windows.
useQuickChat restoration with server validation
packages/dashboard/app/hooks/useQuickChat.ts, packages/dashboard/app/hooks/__tests__/useQuickChat.test.ts
Applies the same server-validation pattern: fetches session state after restoring pending message, conditionally attaches to in-flight stream when generation is active, and defers flushing to stream completion. Also narrows session-activation auto-flush so only pre-session queued sends auto-flush. Regression tests validate both active-generation attachment and pending-fetch behavior.

Sequence Diagram

sequenceDiagram
  participant User as User/Client
  participant Hook as Chat Hook
  participant Server as fetchChatSession
  participant Stream as attachChatStream
  User->>Hook: navigate back, restore pending message
  Hook->>Server: fetch authoritative session state
  Server-->>Hook: session.isGenerating status
  alt generation is active
    Hook->>Stream: attach/replay to in-flight stream
    Stream-->>Hook: stream completion/error
    Hook->>Hook: flush queued message on completion
  else no active generation
    Hook->>Hook: flush pending message immediately
  end
  Hook-->>User: queued message sent or retained
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A queue once lost in back-nav's maze,
Now waits for server truth's green blaze,
No stale flags trick the restoration dance—
Fetch, confirm, attach, and give messages their chance!
With tests to catch what silence hides,
The queued reply now safely rides! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the core fix: confirming server generation state before flushing queued chat messages, which directly addresses the main objective.
Linked Issues check ✅ Passed The PR comprehensively addresses all requirements from issue #1279 (FN-5852): prevents queued message loss, ensures persistence during re-entry, and replaces stale client-side isGenerating checks with server confirmation.
Out of Scope Changes check ✅ Passed All changes directly support the stated objective: hook implementations, regression tests, documentation, and metadata are all focused on server-confirmation logic for queued message flushing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gsxdsm/queued-messages

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…ncepts

Document why the FN-5852 queued-message loss survived two fixes (client
gates a destructive flush on a route-level enrichment field that SSE
payloads lack) and seed CONCEPTS.md with Generation, Queued message, and
Enrichment field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a race condition in both useChat and useQuickChat where a restored queued follow-up message was flushed immediately on session re-entry using a stale local isGenerating flag (which is a route-level enrichment absent from SSE payloads). The premature send would abort the live server-side generation and delete the persisted copy before the send could fail.

  • Core fix: the restore effect in both hooks now calls fetchChatSession before flushing — if the server reports an active generation it re-attaches to the stream and lets onDone/onError deliver the queued message; if idle it flushes immediately; if the fetch fails the bubble is kept for a later trigger.
  • Guard in useQuickChat: a queuedPreSessionCompletionRef check was added to the session-activation auto-flush effect so it only fires for pre-session queues, not restored messages, preventing a race ahead of the authoritative server check.
  • Regression tests: both hook suites now model the production-reachable state (stale-falsy isGenerating in local state, server actively generating) that the prior two rounds of fixes never exercised.

Confidence Score: 5/5

Safe to merge — the changes narrow a destructive action (sending a message that aborts a live generation) to only fire after server confirmation, and add a cleanup flag to prevent stale async callbacks from acting on switched sessions.

Both hooks now gate the queued-message flush on a live server fetch, with a proper cancelled cleanup on effect teardown. The queuedPreSessionCompletionRef guard in useQuickChat correctly separates the pre-session queue path from the restored-message path, preventing a race that the previous fix left open. The new regression tests model the previously-unreachable production fixture and assert no premature send, intact localStorage, and correct flush after stream completion.

No files require special attention.

Important Files Changed

Filename Overview
packages/dashboard/app/hooks/useChat.ts Restore effect replaced queueMicrotask flush with server-authoritative fetchChatSession check; adds cancelled cleanup flag and re-attaches stream if generating — logic is sound and dependencies array is complete.
packages/dashboard/app/hooks/useQuickChat.ts Mirrors the useChat restore-effect fix and adds a queuedPreSessionCompletionRef guard to the session-activation auto-flush effect to prevent it from racing with the new server validation round-trip.
packages/dashboard/app/hooks/tests/useChat.test.ts Two new regression tests correctly model the previously-unreachable production state (stale-falsy isGenerating + server actively generating) and assert no premature send, intact localStorage, and correct flush after stream completion.
packages/dashboard/app/hooks/tests/useQuickChat.test.ts Mirrors useChat regression tests plus a specific test for the auto-flush guard; the 25ms setTimeout in the pending-fetch test is a minor timing dependency but follows existing patterns in the suite.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant Hook as useChat / useQuickChat
    participant LS as localStorage
    participant S as Server

    U->>Hook: navigate back + re-enter session
    Hook->>LS: getPersistedPendingChatMessage(sessionId)
    LS-->>Hook: Queued follow-up
    Hook->>Hook: restore pendingMessage ref + state
    Note over Hook,S: New: server-authoritative check
    Hook->>S: fetchChatSession(sessionId)
    alt Server still generating
        S-->>Hook: isGenerating true
        Hook->>Hook: attachIfGenerating
        Hook-->>U: restored bubble + Connecting
        Hook->>Hook: stream onDone fires
        Hook->>S: streamChatResponse
        Hook->>LS: removePersistedPendingChatMessage
    else Server idle
        S-->>Hook: isGenerating false
        Hook->>S: streamChatResponse immediate
        Hook->>LS: removePersistedPendingChatMessage
    else Fetch failed
        S-->>Hook: error
        Note over Hook: keep bubble for later trigger
    end
Loading

Reviews (3): Last reviewed commit: "Update packages/dashboard/app/hooks/__te..." | Re-trigger Greptile

Comment thread packages/dashboard/app/hooks/useQuickChat.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/dashboard/app/hooks/__tests__/useQuickChat.test.ts (1)

975-1028: ⚡ Quick win

Consider verifying attachChatStream call before triggering its handler.

The test correctly models the FN-5852 regression scenario, but line 1018 accesses attachHandlers[0]?.onDone without first asserting that attachChatStream was called. If the attach flow doesn't execute as expected, the test would time out waiting for streamChatResponse rather than failing with a clear assertion.

✅ Suggested verification before triggering the handler
     expect(mockStreamChatResponse).not.toHaveBeenCalled();
     expect(localStorage.getItem(getChatPendingMessageKey("session-a"))).toBe("Queued follow-up");
 
+    // Verify that the hook attached to the in-flight generation
+    expect(mockAttachChatStream).toHaveBeenCalledTimes(1);
+    expect(mockAttachChatStream).toHaveBeenCalledWith(
+      "session-a",
+      expect.any(Object),
+      "proj-123",
+      expect.objectContaining({ lastEventId: expect.any(Number) }),
+    );
+
     // Once the attached generation completes, the queued message flushes.
     act(() => {
       attachHandlers[0]?.onDone?.({ messageId: "msg-001" });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/app/hooks/__tests__/useQuickChat.test.ts` around lines 975
- 1028, The test calls attachHandlers[0]?.onDone without first asserting that
mockAttachChatStream actually ran; add a verification step before triggering the
handler (e.g., expect(mockAttachChatStream).toHaveBeenCalled() or
expect(attachHandlers.length).toBeGreaterThan(0)) to fail fast if the attach
flow didn't execute, then safely call attachHandlers[0].onDone({ messageId:
"msg-001" }) to simulate completion; reference the mockAttachChatStream mock and
the attachHandlers array/its onDone handler when adding the assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@docs/solutions/logic-errors/queued-chat-message-flush-trusts-stale-isgenerating.md`:
- Around line 1-27: The frontmatter is missing the required applies_when field;
open the YAML frontmatter at the top of this document and add an applies_when
key with a concise condition describing when the solution applies (e.g., the
reproduction trigger for this logic error), ensuring it sits alongside existing
keys like category, module, and problem_type so the frontmatter contains
category, module, problem_type, and applies_when.

In `@packages/dashboard/app/hooks/useQuickChat.ts`:
- Around line 659-696: The later auto-flush that calls flushPendingMessage is
not guarded against a newly restored pendingMessageRef.current set
synchronously, so the effect at activeSession (the one around
flushPendingMessage / isStreamingRef / streamRef) can run and send a stale queue
before fetchChatSession completes; update that later flush path to only proceed
for pre-session queued messages by checking
queuedPreSessionCompletionRef.current !== null (or another boolean that is set
before restoring pendingMessageRef.current) or otherwise suppress the flush
until this server validation finishes (i.e., add a “validationInProgress” guard
set before calling fetchChatSession and cleared in its then/catch, and have the
flush effect return early if validationInProgress is true); reference
pendingMessageRef.current, queuedPreSessionCompletionRef.current,
fetchChatSession, activeSession, isStreamingRef, streamRef, flushPendingMessage,
and attachIfGenerating when making the guard change.

---

Nitpick comments:
In `@packages/dashboard/app/hooks/__tests__/useQuickChat.test.ts`:
- Around line 975-1028: The test calls attachHandlers[0]?.onDone without first
asserting that mockAttachChatStream actually ran; add a verification step before
triggering the handler (e.g., expect(mockAttachChatStream).toHaveBeenCalled() or
expect(attachHandlers.length).toBeGreaterThan(0)) to fail fast if the attach
flow didn't execute, then safely call attachHandlers[0].onDone({ messageId:
"msg-001" }) to simulate completion; reference the mockAttachChatStream mock and
the attachHandlers array/its onDone handler when adding the assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 00021d7a-ecf6-4d41-9b1e-0fde708e862b

📥 Commits

Reviewing files that changed from the base of the PR and between 07dd24e and 88a2c5a.

📒 Files selected for processing (7)
  • .changeset/fn-5852-queued-message-stale-flush.md
  • CONCEPTS.md
  • docs/solutions/logic-errors/queued-chat-message-flush-trusts-stale-isgenerating.md
  • packages/dashboard/app/hooks/__tests__/useChat.test.ts
  • packages/dashboard/app/hooks/__tests__/useQuickChat.test.ts
  • packages/dashboard/app/hooks/useChat.ts
  • packages/dashboard/app/hooks/useQuickChat.ts

Comment thread packages/dashboard/app/hooks/useQuickChat.ts
- gate Quick Chat's session-activation auto-flush to the pre-session
  queue only, so a restored queue cannot be sent before the restore
  effect's authoritative fetchChatSession check resolves (real flaw —
  the original test passed only because the mocked fetch resolved in a
  microtask and beat the effect)
- add slow-fetch regression tests in both hooks proving the restored
  queue stays un-flushed while server validation is pending
- assert attachChatStream ran before triggering its onDone in the
  quick-chat regression test
- drop redundant Promise.resolve() wrapper around fetchChatSession in
  useQuickChat's restore effect

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gsxdsm

gsxdsm commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

Consider verifying attachChatStream call before triggering its handler. (CodeRabbit nitpick, useQuickChat.test.ts)

Addressed in 47bd46e — the regression test now asserts mockAttachChatStream was called (and attachHandlers is non-empty) before triggering onDone, so an attach-flow failure surfaces as a clear assertion instead of a timeout.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dashboard/app/hooks/__tests__/useChat.test.ts`:
- Around line 2176-2178: Replace the non-deterministic act + setTimeout sleep
with a deterministic waitFor that asserts the hook's expected state change:
import and use waitFor (from `@testing-library/react`) instead of new Promise;
wait until the hook restores pendingMessage and ensures streamChatResponse is
falsy/cleared (reference the pendingMessage and streamChatResponse
variables/state retrieved from useChat in this test), then proceed — this
removes the 25ms wall-clock sleep and reliably waits for the intended state
transition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b3311c0-3319-4534-9f40-8f6fd047ef2c

📥 Commits

Reviewing files that changed from the base of the PR and between 88a2c5a and 47bd46e.

📒 Files selected for processing (3)
  • packages/dashboard/app/hooks/__tests__/useChat.test.ts
  • packages/dashboard/app/hooks/__tests__/useQuickChat.test.ts
  • packages/dashboard/app/hooks/useQuickChat.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/dashboard/app/hooks/useQuickChat.ts
  • packages/dashboard/app/hooks/tests/useQuickChat.test.ts

Comment thread packages/dashboard/app/hooks/__tests__/useChat.test.ts Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@gsxdsm
gsxdsm merged commit cc2ea4a into main Jun 4, 2026
8 checks passed
@gsxdsm
gsxdsm deleted the gsxdsm/queued-messages branch June 4, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FN-5852] Queued messages disappear if you leave the interface before they are sent.

1 participant