Skip to content

📬 fix: Drain a Follow-Up Queued in a Chat the User Left - #16632

Open
berry-13 wants to merge 6 commits into
devfrom
berry-13/queue-drain-on-return
Open

berry-13 wants to merge 6 commits into
devfrom
berry-13/queue-drain-on-return

Conversation

@berry-13

@berry-13 berry-13 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

If you queue a follow-up while a reply is generating, then switch to another chat before the reply finishes, the follow-up never sends. When you come back, the finished reply loads from history, but the follow-up stays in the queue rail until you send it by hand. If you stay in the chat, it sends automatically. #16615 exposed this while writing queue coverage, and it reproduces on code from before that change.

The cause: starting another chat clears the pane's submission. The stream hook then closes the stream without stopping the server run, which is deliberate. The server deletes the job when it finishes. So no terminal event reaches the queue drain, and the resume-on-load path that reloads the finished reply does not publish a run end.

The stream hook now remembers a run it closed while that run was still generating. On return, if the server reports nothing active, the resume path reads the run's persisted response and parks its end for the drain. A completed reply then drains the queue, as it would for an attached run. A failed reply leaves the queue for a manual send. A run the user stopped, or whose end already reached the drain, is not remembered. A stopped reply persists like a completed one, so the stop request itself is what keeps a stopped run from auto-sending.

Closes berry-13#225.

Type of change

  • Bug fix

Testing

Tested environments/configuration:

  • Jest (jsdom). Playwright mock harness through reviewctl verify (desktop light, desktop dark, mobile).

Automated tests:

  • Scenario parked-run-end-drains-on-return in queue-owners.spec.ts: queue a follow-up during a slow run, leave the chat, wait for the server to finish the run, then return. The follow-up is sent. The same scenario failed on canary before this fix, which is why 🧺 refactor: Move the Chat Follow-Up Queue State to Jotai #16615 dropped it.
  • useResumeOnLoad.spec: a detached run that finished parks a completed run end and clears the marker. A conversation with no detached run parks nothing. The first test fails with the fix removed.
  • useResumableSSE.spec: navigation records the detached run. A terminal clear, a run end that already reached the drain, and a stopped run record nothing.
  • queue.spec: outcome resolution covers completed, failed, unfinished, missing and sibling responses.
  • npx jest --findRelatedTests <changed files> --maxWorkers=2: 499 suites, 6593 tests. npx tsc --noEmit -p client/tsconfig.json: clean. ESLint and Prettier: clean.

Screenshots / recordings

No visual change; the e2e scenario covers the behavior.

Risk / compatibility

A run that started as a brand-new conversation is not tracked, because its submission carries no concrete conversation id; it keeps today's behavior. The marker is session memory only, like the queue it serves.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required dependency changes have been merged/published
  • Required documentation PR: N/A

Copilot AI balanced review requested due to automatic review settings October 1, 2026 16:39
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T10:05:56.826084Z 0dfc5d2 New commits
🔒 Security Review ✅ Completed 2026-10-01T16:43:21.796629Z cb3d653 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Existing-conversation navigation still drops the marker, and response-resolution races can strand queued follow-ups.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity

Open (3)
What changed in this PR

Fixes queued follow-ups not draining after leaving a conversation during generation.

Changes:

  • Tracks detached and explicitly stopped runs.
  • Reconstructs run completion from persisted history.
  • Adds unit, hook, and end-to-end coverage.
File Description
e2e/​specs/​mock/​scenarios/​queue-owners.spec.ts Tests draining after returning.
client/​src/​hooks/​SSE/​useResumeOnLoad.ts Resolves detached run endings.
client/​src/​hooks/​SSE/​useResumableSSE.ts Records detached active runs.
client/​src/​hooks/​SSE/​__tests__/​useResumeOnLoad.spec.tsx Tests run-end restoration.
client/​src/​hooks/​SSE/​__tests__/​useResumableSSE.spec.ts Tests detachment conditions.
client/​src/​hooks/​Chat/​useChatHelpers.ts Records explicit Stop requests.
client/​src/​hooks/​Chat/​queue.ts Adds detached-run state and resolution.
client/​src/​hooks/​Chat/​__tests__/​queue.spec.ts Tests response outcome resolution.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread client/src/hooks/Chat/queue.ts Outdated
Comment on lines +209 to +215
const target = run.responseMessageId?.replace(/_+$/, '');
const responses = (messages ?? []).filter(
(message) => message.isCreatedByUser === false && message.parentMessageId === run.userMessageId,
);
const response =
responses.find((message) => message.messageId === target) ??
(responses.length === 1 ? responses[0] : undefined);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 16e5b7b: an explicit response id is matched exactly first, then without its trailing underscores; the single-reply fallback applies only when the run named no response (queue.spec covers an id ending in _).

Comment thread client/src/hooks/SSE/useResumableSSE.ts Outdated
Comment on lines +4288 to +4290
if (submission != null && detachCandidate != null) {
jotaiStore.set(detachedRunByConvoId(detachCandidate.conversationId), detachCandidate.run);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 16e5b7b: the cleanup now records any run it closes while still generating, whatever cleared the submission (null from useNavigateToConvo, {} from a new chat, or an unmount); scenario parked-run-end-drains-after-switching-chats covers the sidebar switch.

Comment thread client/src/hooks/SSE/useResumeOnLoad.ts Outdated
Comment on lines +1013 to +1015
if (detachedRun != null) {
jotaiStore.set(detachedFamily, null);
const end = resolveDetachedRunEnd(conversationId, detachedRun, getMessages());

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 16e5b7b: the marker is cleared only once its response is found; an unresolved run keeps it and gets one history refetch (useResumeOnLoad.spec: keeps the detached run until a history refetch shows its response).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb3d653edf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/SSE/useResumableSSE.ts Outdated
* the queue drain; remember the run, and its end is read from history on return. */
const detachCandidate = detachCandidateRef.current;
detachCandidateRef.current = null;
if (submission != null && detachCandidate != null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Record detachments when switching to a saved chat

When the user leaves via the normal saved-conversation navigation, useNavigateToConvo clears the submission with setSubmission(null) (client/src/hooks/Conversations/useNavigateToConvo.tsx:238), rather than with the empty object used by the new-chat path. The cleanup has already captured the live run in detachCandidateRef, but this condition rejects and then discards that candidate, so returning after the run finishes cannot publish its end and the queued follow-up remains stuck. The new test only exercises navigation to /c/new, so it misses the standard chat-to-chat flow.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 16e5b7b: switching to a saved chat (setSubmission(null)) now records the detached run; covered by parked-run-end-drains-after-switching-chats and the null-clear case in useResumableSSE.spec.

Comment thread client/src/hooks/SSE/useResumeOnLoad.ts Outdated
Comment on lines +1014 to +1015
jotaiStore.set(detachedFamily, null);
const end = resolveDetachedRunEnd(conversationId, detachedRun, getMessages());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain the detached marker until its response is loaded

If the user returns while the run is finishing, the messages request can read history before the response is persisted, while the subsequent status request observes that the job has already become inactive. In that case resolveDetachedRunEnd returns null, but the marker is cleared first and processedConvoRef is then latched, so no later history refresh can retry the resolution and the follow-up remains queued indefinitely. Clear the marker only after a terminal response has actually been resolved, or explicitly refetch/re-arm when it is absent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 16e5b7b: the marker stays until a response resolves it, with one history refetch for a response saved after the first read.

@codegraph-librechat codegraph-librechat Bot added the 🗺️ Chat State Mgmt codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Oct 1, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16e5b7b798

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +219 to +221
response =
responses.find((message) => message.messageId === exact) ??
responses.find((message) => message.messageId === unpadded);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject the pre-run regeneration row as terminal

When a regeneration is left and its history request completes before terminal persistence, the messages still contain the previous response under the unpadded ID. This fallback immediately accepts that old row as the detached run's result and usually classifies it as completed, so a regeneration that subsequently aborts or fails can auto-send the queued follow-up instead of leaving it for manual send. The fresh evidence beyond the earlier missing-response race is that regenerations deliberately use existingId_ while the pre-run existingId row already exists, meaning parkDetachedEnd does not take the new refetch/retry path at all. Require evidence that this row was updated by the detached generation before publishing its outcome.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d8269cc: a detached regeneration is no longer resolved from history (resolveDetachedRunEnd returns null for isRegenerate), so the follow-up stays queued for a manual send; covered in queue.spec and useResumableSSE.spec.

Comment thread client/src/hooks/SSE/useResumableSSE.ts
Comment on lines +234 to +238
return {
conversationId,
outcome,
endedAt: Date.now(),
...(outcome === 'completed' && { responseMessageId: response.messageId }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the detached generation epoch

When an Agent server-queued turn is admitted while the predecessor chat is detached, its receipt records the consumed predecessor in effectivePredecessorCreatedAt, but this synthesized end omits generationCreatedAt. useQueueDrain therefore cannot match and consume that admission receipt; once receipt reconciliation removes the admitted server row, any remaining local row is treated as eligible on this predecessor end and can be submitted while the server-started successor already owns the conversation. Capture the detached run's active generation epoch and include it in the reconstructed RunEnd so the existing admission-boundary guard remains effective.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d8269cc: the pane captures the run's live generation epoch when it leaves and the parked end carries it as generationCreatedAt, so the drain still matches admission receipts; a detached run with no epoch whose queue the server shares is left for a manual send (useResumeOnLoad.spec covers both).

Leaving a chat mid-run closes its stream without stopping the run, and
the server deletes the job when it finishes, so no run end ever reached
the queue drain and the follow-up stayed in the rail. The stream hook now
remembers a run it closed while still generating, and on return the
resume path reads the run's persisted response and parks its end, so a
completed run drains the queue as an attached one would. A run the user
stopped, or whose end already reached the drain, is not remembered.
…as It

Switching to a saved chat clears the submission to null, and unmounting
runs no later effect body, so the detached run is now recorded in the
cleanup itself whenever the stream closes on a run that is still going.
A new run in the conversation clears it. On return the marker is kept
until its response is in history, with one history refetch for a
response saved after the first read, and an explicit response id is
matched exactly before its unpadded form, never by a sibling fallback.
A regeneration rewrites a response history already holds, so a detached
one is no longer resolved from history. The run's generation epoch is
captured when the pane leaves it and carried into the parked run end,
so the drain still matches server admission receipts; a run with no
epoch whose queue the server shares is left for a manual send.
@berry-13
berry-13 force-pushed the berry-13/queue-drain-on-return branch from 16e5b7b to 071d82a Compare October 2, 2026 08:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 071d82a38e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/SSE/useResumableSSE.ts
Comment thread client/src/hooks/Chat/queue.ts
@berry-13

berry-13 commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 071d82a38e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

jotaiStore.set(detachedFamily, null);
return true;
}
const end = resolveDetachedRunEnd(conversationId, detachedRun, getMessages());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate detached outcomes against persisted history

When the user leaves after the created event, the optimistic assistant placeholder remains in the React Query cache, whose messages query has refetchOnMount: false. If the server later aborts or errors, this inactive path passes that cached row to resolveDetachedRunEnd before performing any refetch; because the optimistic row lacks the persisted unfinished/error flags, it is classified as completed and the queued follow-up is sent unexpectedly. Unlike the earlier missing-response race, the stale cache supplies a non-null response, so the new fallback refetch never runs; require fresh persisted history or other terminal evidence before publishing the completion.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 0dfc5d2: a detached run is now resolved only after a successful history refetch (throwOnError), never from the optimistic row left in cache; a failed refetch keeps the marker for the next visit. useResumeOnLoad.spec adds an aborted-while-away case and a failed-refetch case, both failing on 071d82a.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0dfc5d26cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/SSE/useResumeOnLoad.ts

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🗺️ Chat State Mgmt codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants