Skip to content

⏯️ feat: Resume Streams and Send AI SDK Messages Through the Chat Facade - #16609

Merged
berry-13 merged 7 commits into
devfrom
berry-13/chat-facade-resume
Oct 1, 2026
Merged

berry-13 merged 7 commits into
devfrom
berry-13/chat-facade-resume

Conversation

@berry-13

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

Copy link
Copy Markdown
Collaborator

Pull Request

Before submitting, please review the Contributing Guide.

Documentation changes belong in the LibreChat documentation repository.

Summary

The chat facade (useChat, useChatActions) covered seven of the eight AI SDK useChat members but had no resumeStream, and its sendMessage only took LibreChat's ask arguments. This covers chat audit criterion C6.

resumeStream asks the pane to reattach to its running generation. It does not open a second resume path: it bumps a per-conversation request that useResumeOnLoad answers the way it answers an announced job. It re-reads the stream status and builds the resume submission, which useResumableSSE attaches through the host transport from #16592. It does nothing for a new chat or one already attached, and waits while the pane is still loading the conversation the route names. A request for a conversation no chat view shows stays pending until one does; opening it runs the same status check either way.

sendMessage now also takes an AI SDK user message of text parts (with optional parentMessageId metadata; null attaches at the root) and sends it as the ask call it describes. Like AI SDK useChat, it goes to the facade's own chat, so it names no conversation. { text } was already valid for both. File parts are left out of the type because a turn takes its files from the composer. TAskFunction callers are unchanged.

Type of change

  • Feature

Testing

Tested environments/configuration:

  • lc dev servers on this stack, Chromium (Playwright, headless), an agent on Anthropic claude-sonnet-4-6: send, regenerate, stop (abort 200), steer, queue, and reload mid-stream (reattached with resume=true and finished). No page errors.

Automated tests:

  • facade.spec.tsx: AI SDK message to ask, a refused send returns false, a failed stop rejects, and resumeStream requests a resume for the chat and none for a new chat (39 tests).
  • transport.spec.tsx: with real useChatHelpers, useResumeOnLoad, useResumableSSE and useChat over a fake transport, resumeStream reattaches to a running generation, re-reads the status and stays detached when nothing runs, and reports a reattached stream that fails as status: 'error' with its message (16 tests). These fail without the useResumeOnLoad change.
  • npx jest --findRelatedTests on the changed files: 492 of 493 suites pass. The one failure, DeploymentTheme.spec.tsx ("resolves a bundled theme name without touching stored preferences"), fails the same way on canary.
  • npx tsc --noEmit -p client/tsconfig.json: clean. npm run static-checks -- --against origin/canary: passed.

Screenshots / recordings

No user-facing change.

Risk / compatibility

sendMessage is no longer the ask function itself but a stable wrapper over it (same dependencies), so memoized consumers see the same identity stability. resumeStream resolves when the request is made, not when the stream ends.

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

@berry-13 berry-13 changed the title berry-13/chat-facade-resume ⏯️ feat: Resume Streams and Send AI SDK Messages Through the Chat Facade Oct 1, 2026
@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-01T16:59:32.299513Z a1b9dd6 New commits
🔒 Security Review ✅ Completed 2026-10-01T10:08:30.737474Z 094097c 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.

@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: 094097c080

ℹ️ 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/store/resume.ts Outdated
* `resumeStream`). `useResumeOnLoad` answers each increase with the same status re-check it runs
* when a job is announced, so a request never builds a second resume path.
*/
export const resumeRequestFamily = atomFamily((_conversationId: string) => atom<number>(0));

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 Keep the resume request atom inside the chat feature

This atom is written by the chat facade and read only by the chat resume path, so placing it under the app-global ~/store makes feature-owned state part of the shell and preserves the coupling that prevents the chat feature from moving to its own workspace. Define the signal within the chat feature, or pass it through a host-supplied context, rather than adding it to the global store.

AGENTS.md reference: AGENTS.md:L161-L170

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 1997742 and 592cdae: the resume request now lives in client/src/hooks/Chat/resume.ts with the chat feature, as one atom holding the conversation id and a count. reviewctl verify passed all four contract scenarios on 592cdae (desktop light, desktop dark, mobile).

@berry-13
berry-13 added this pull request to stack #16614 October 1, 2026 10:32
@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: 592cdaeb3d

ℹ️ 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/Chat/facade.ts Outdated
if (!id || id === Constants.NEW_CONVO) {
return;
}
jotaiStore.set(resumeRequestAtom, ({ count }) => ({ conversationId: id, count: count + 1 }));

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 Preserve resume requests for every visible pane

When two multi-conversation panes call resumeStream() before React flushes effects—for example via Promise.all or consecutive awaited calls—both writes update the count, but this single slot retains only the second conversationId. The first pane then observes the new count, marks it answered, rejects it because the conversation ID belongs to the other pane, and never rechecks its stream status even though its promise resolved. Store pending counts per conversation or otherwise queue requests so one pane cannot overwrite another's request.

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 ffc3634: pending resume requests are now a set of conversation ids. The facade adds its chat, and the pane showing that conversation consumes only its own entry, so a second pane's request no longer overwrites the first. transport.spec covers one pane answering while another pane's request stays pending; reviewctl verify passed on that head.

@berry-13
berry-13 force-pushed the berry-13/chat-facade-resume branch from ffc3634 to 968d601 Compare October 1, 2026 11:48

@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: 968d6010c0

ℹ️ 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 +377 to +379
jotaiStore.set(resumeRequestsAtom, (pending) =>
pending.has(id) ? pending : new Set(pending).add(id),
);

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 Scope resume requests to the originating pane

When comparison mode displays the same conversation in two panes, this conversation-only key makes both mounted useResumeOnLoad instances observe the request before either passive effect removes it, so both pane indices can build submissions and open duplicate resumable SSE connections even though only one facade called resumeStream(). Fresh evidence beyond the earlier overwritten-request report is useSteerRecovery.spec.tsx:610-620, which explicitly establishes that comparison mode can hold the same conversation in two panes; include the pane/run index in the request identity or otherwise route the request to its originating pane.

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.

Not applicable on f9e181f: useResumeOnLoad has one mount site (ChatView.tsx:125), and ChatRoute renders a single ChatView with index hard-coded to 0 (ChatRoute.tsx:63). The comparison pane is the added response, which never mounts the resume path, so only one pane can consume a request for a conversation.

Comment thread client/src/hooks/Chat/facade.ts Outdated
Comment on lines +361 to +362
...(conversationId !== undefined && { conversationId }),
...(parentMessageId !== undefined && { parentMessageId }),

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 Preserve an explicit root parent when sending UI messages

When an AI SDK-shaped message carries metadata.parentMessageId: null—the value emitted for a stored root message—this forwards the null even though useChatFunctions.ask treats parentMessageId == null as “unspecified” and replaces it with the currently viewed tail (useChatFunctions.ts:561-563). Consequently, resending or constructing a message explicitly attached to the root instead appends it to the active branch, despite SendMessageInput documenting this metadata as where the turn attaches; translate an explicit null to the root sentinel or distinguish omission from null before invoking ask.

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 f9e181f: an explicit null parentMessageId now maps to the no-parent sentinel before ask, so a root-attached message stays at the root; facade.spec covers it. reviewctl verify passed on that head.

@berry-13
berry-13 force-pushed the berry-13/chat-facade-resume branch from f9e181f to a5ffcf7 Compare October 1, 2026 14:16
@berry-13
berry-13 requested a review from danny-avila as a code owner October 1, 2026 14:16
@berry-13
berry-13 removed this pull request from stack #16614 October 1, 2026 14:17
@berry-13
berry-13 changed the base branch from canary to dev October 1, 2026 14:17
@berry-13
berry-13 added this pull request to stack #16626 October 1, 2026 14:17

@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: a5ffcf7455

ℹ️ 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 +1253 to +1254
if (!resumableEnabled || conversationId === Constants.NEW_CONVO) {
return;

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 Defer consuming resumes until the route conversation is loaded

When navigating from an Assistants conversation to a resumable agent conversation, ChatRoute keeps the old ChatView mounted while the new conversation loads, so resumeStream() is already keyed to the new route via messagesKey while resumableEnabled still reflects the old Assistants endpoint. This branch returns after the request was deleted above, and the request is not retried when the target conversation arrives, so a running generation remains detached. Retain the request until currentConversation.conversationId matches conversationId, or otherwise avoid consuming it for this transient mismatch.

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 0bb672c: a resume request now waits until the pane has loaded the conversation the route names, so a pane still holding the previous Assistants conversation no longer consumes it. transport.spec covers that switch (request held, then answered and reattached) and fails without the guard; reviewctl verify passed on that head.

Comment thread client/src/hooks/Chat/facade.ts Outdated
const { conversationId, parentMessageId } = message.metadata ?? {};
return {
text: message.parts.map((part) => part.text).join(''),
...(conversationId !== undefined && { conversationId }),

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 Reject cross-chat metadata before forwarding sendMessage

When an AI SDK-shaped message names a conversationId different from the facade's current chat, forwarding it here makes ask load that target conversation's messages but still construct the request from the facade's active immutableConversation (useChatFunctions.ts:384,409,610-639). The turn can therefore use the wrong endpoint/model/agent settings, and when parentMessageId is omitted it also derives the parent from the current chat's latestMessage rather than the target history. Either require this metadata ID to equal the facade ID or resolve the complete target conversation before sending.

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 0bb672c: the AI SDK input's metadata now carries only parentMessageId, so a sendMessage call goes to the facade's own chat, as AI SDK useChat does. TAskProps callers are unchanged.

Copilot AI balanced review requested due to automatic review settings October 1, 2026 15:46

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

Message metadata handling and unmounted-pane resume behavior contradict the documented contract.

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

Open (3)
What changed in this PR

Adds AI SDK-compatible sending and resumable streams to the client chat facade.

Changes:

  • Maps text-part messages into LibreChat ask requests.
  • Adds pane-mediated resumeStream.
  • Adds facade and transport integration tests.
File Description
client/​src/​hooks/​Chat/​facade.ts Extends facade actions and message mapping.
client/​src/​hooks/​Chat/​resume.ts Stores pending resume requests.
client/​src/​hooks/​SSE/​useResumeOnLoad.ts Consumes explicit resume requests.
client/​src/​hooks/​Chat/​__tests__/​facade.spec.tsx Tests facade actions.
client/​src/​hooks/​Chat/​__tests__/​transport.spec.tsx Tests resume transport behavior.

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

*/
export type SendMessageInput = {
parts: UITextPart[];
metadata?: Partial<Pick<UIMessageMetadata, 'parentMessageId'>>;

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.

Not applicable on a1b9dd6: the facade sends to its own chat, as AI SDK useChat does. A cross-chat conversationId would make ask load one conversation while building the request from another (the earlier finding on this PR), so it was removed on purpose; the PR body's stale sentence is corrected.

Comment on lines +381 to +383
jotaiStore.set(resumeRequestsAtom, (pending) =>
pending.has(id) ? pending : new Set(pending).add(id),
);

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.

Accepted on a1b9dd6: a request for a conversation no pane shows stays pending, and opening that conversation runs the same status check, so answering it changes nothing. Entries exist only for chats a host asked to resume. The PR body now says this instead of claiming a no-op.

Comment on lines +697 to +704
const store = getDefaultStore();
store.set(resumeRequestsAtom, new Set(['convo-2']));
const { result } = renderChat(createContract());

await result.current.resumeStream();
await result.current.resumeStream();

expect([...store.get(resumeRequestsAtom)]).toEqual(['convo-2', 'convo-1']);

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 a1b9dd6: both resume tests render under IsolatedAtomStore and assert against that store.

useChat and useChatActions gain resumeStream, which asks the pane's resume-on-load path
to re-check the stream status and reattach through the host transport, and sendMessage
now also takes an AI SDK user message of text parts, sent as the ask call it describes.
The facade spec covers the new members and the refused-send and failed-stop paths; the
transport spec resumes a running generation against a fake transport, stays detached
when nothing runs, and reports a reattached stream that fails.
The resume-request atom is written by the facade and read by the chat's resume path,
so it moves from the app store to hooks/Chat.
A request only matters to the pane showing its conversation, so a single atom with the
conversation id and a count replaces the per-conversation family, which kept an atom for
every conversation a session ever asked to resume.
A single request slot let a second pane's resumeStream overwrite the first before
either pane's effect ran, so the first request was dropped. Requests are now a set of
conversation ids: the facade adds its chat, and the pane showing that conversation
consumes the entry and answers it.
A root message's view carries a null parentMessageId, which ask reads as unspecified
and replaces with the branch tail. sendMessage now passes the no-parent sentinel for an
explicit null, so the turn attaches where its metadata says.
…the Facade's Chat

A pane navigating away from an Assistants conversation still reads that endpoint while
the route already names the next one, so it consumed a resume request it could not answer.
The request now waits until the pane has loaded the route's conversation. sendMessage's AI
SDK input also stops accepting a conversationId: the turn goes to the facade's own chat.
@berry-13
berry-13 force-pushed the berry-13/chat-facade-resume branch from 0bb672c to 8da1638 Compare October 1, 2026 16:19
The resume-request tests wrote to jotai's module-global default store and left their
entries for later tests; they now render under IsolatedAtomStore and read that store.
@berry-13
berry-13 merged commit 76b0ea8 into dev Oct 1, 2026
30 checks passed
@berry-13
berry-13 deleted the berry-13/chat-facade-resume branch October 1, 2026 17:21
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