Skip to content

🪝 feat: AI SDK-Shaped useChat Facade Over the Chat Contract - #16375

Open
berry-13 wants to merge 5 commits into
berry-13/chat-partsfrom
berry-13/chat-facade
Open

berry-13 wants to merge 5 commits into
berry-13/chat-partsfrom
berry-13/chat-facade

Conversation

@berry-13

Copy link
Copy Markdown
Collaborator

Summary

The chat context is already an explicit contract, but consumers still read it in LibreChat terms (ask, isSubmitting, getMessages). This adds useChat in client/src/hooks/Chat/facade.ts, which presents the same contract the way @ai-sdk/react@4.0.117 does. It sits on top of the parts mapping from the base PR.

It returns id, messages (getMessages() mapped through toUIMessage), status, error, sendMessage, regenerate, stop and setMessages. status is submitted while a turn is in flight and its response has no content yet, then streaming. Once the turn ends it is error if the latest message failed and ready otherwise, which includes a stopped turn. abortScroll only holds the scroll position, so a stop is read from the settled message instead. sendMessage is ask itself, stop is stopGenerating, regenerate({ messageId }) resolves the target the contract expects, and setMessages writes UI messages back onto the stored ones. The hook holds no state and reads no store, and no existing consumer changes.

Depends on #16374, which this is stacked on.

Type of change

  • Feature

Testing

Tested environments/configuration:

Unit level only; no component uses the facade yet.

Automated tests:

  • Added client/src/hooks/Chat/__tests__/facade.spec.tsx: renders under the real ChatContext with a stubbed contract; status through submit, stream, finish, abort and error; sendMessage forwards to ask with the same arguments; regenerate, stop and setMessages forward
  • cd client && npx jest hooks/Chat: 14 suites, 365 passed
  • cd client && npm run typecheck: clean
  • npm run static-checks -- --against origin/canary: all affected checks passed

Screenshots / recordings

No user-facing change.

Risk / compatibility

None. messages follows getMessages(), so it is only as fresh as the host's re-render, the same as the contract it reads.

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 documentation PR: N/A

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 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 🔄 Running since 2026-09-26T11:15:14.016711Z 2254d21 New commits
🔒 Security Review ✅ Completed 2026-09-25T18:40:16.011681Z f845e79 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: f845e796ef

ℹ️ 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
Comment on lines +32 to +33
const hasStreamed = (message: TMessage) =>
(message.content?.length ?? 0) > 0 || (message.text?.length ?? 0) > 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.

P2 Badge Ignore placeholder-only parts when deriving status

When an Assistants turn starts, useChatFunctions seeds the response with an empty text part, and dual-conversation turns seed type: '' lane placeholders. Testing only content.length therefore reports streaming before any model output arrives, so consumers skip the submitted loading state. Treat empty text/reasoning/tool placeholders as non-streamed content; the existing isEmptyContentPart helper already defines these cases.

AGENTS.md reference: AGENTS.md:L31-L33

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 b49de6e. status reads streamed output through isEmptyContentPart, so empty text/think parts and type '' lane placeholders keep the turn submitted. Covered by 'stays submitted while the response holds only placeholder parts'. The lane placeholder also exposed a crash in toUIMessage, fixed on the base in 16aa523.

Comment on lines +116 to +120
const current = getMessages() ?? [];
const next = typeof update === 'function' ? update(current.map(toUIMessage)) : update;
const byId = new Map(current.map((message) => [message.messageId, message]));
setStoredMessages(next.map((message) => fromUIMessage(message, byId.get(message.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 Make setMessages updates reactive

When an updater changes only a non-tail message, this cache write does not cause useChat to re-render. The provider's React Query subscriptions select only the active tail and its ID, which remain referentially unchanged in that scenario, while this facade reads the full cache imperatively and owns no subscription. As a result, the returned messages remains stale until some unrelated context update occurs, breaking common operations such as editing an earlier message through setMessages; the facade needs to subscribe to the message cache or otherwise schedule an update after the write.

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 b49de6e. messages now reads getMessages through useSyncExternalStore, subscribed to message query cache events, so a setMessages write re-renders without an unrelated context change. No state is added. Covered by 're-reads messages when the message cache is written'.

Comment thread client/src/hooks/Chat/facade.ts Outdated
Comment on lines +39 to +40
const part = message.content?.find((item) => item?.type === ContentTypes.ERROR);
return part?.type === ContentTypes.ERROR ? (part.error ?? '') : '';

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 Read error text from Assistants error parts

When an Assistants run fails, both Assistants error handlers append a part shaped like { type: ContentTypes.ERROR, text: { value: message } }, without an error property or populated top-level message text. This expression consequently returns an empty string, so the facade reports status: 'error' but exposes new Error('') instead of the server's failure message. Extract the error part's text/TextData value as well as its legacy error field.

AGENTS.md reference: AGENTS.md:L31-L33

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 b49de6e. The error text falls back to the error part's error field, then its text (string or TextData value). Covered by 'reads the error text of an Assistants error part'.

Comment thread client/src/hooks/Chat/facade.ts Outdated
Comment on lines +118 to +119
const byId = new Map(current.map((message) => [message.messageId, message]));
setStoredMessages(next.map((message) => fromUIMessage(message, byId.get(message.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 Attach newly inserted UI messages to the conversation

When setMessages is used to append a valid UIMessage that was not produced by this facade, its optional metadata will commonly omit LibreChat's conversationId and parentMessageId. Because there is no matching base, fromUIMessage then stores both fields as null, creating a disconnected root that branch selection and regenerate({ messageId }) cannot operate on. Populate these fields from the active conversation and neighboring message when converting newly inserted messages, or explicitly reject insertion rather than accepting an orphaned message.

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 b49de6e. A message with no stored counterpart gets the active conversation's id and, unless its metadata names a parent, the previous message as parent. Covered by 'joins an inserted message to the conversation under the one before it'.

@berry-13
berry-13 force-pushed the berry-13/chat-facade branch from f845e79 to 6a8a16b Compare September 25, 2026 18:47

@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: 6a8a16be2d

ℹ️ 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
Comment thread client/src/hooks/Chat/facade.ts Outdated
Comment on lines +81 to +82
for (const message of stored ?? []) {
views.push(toUIMessage(message));

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 Cache per-message UI conversions during streaming

When this hook is mounted under the active ChatView, every streamed cache write produces a new stored array and causes this loop to reconvert the entire transcript; toUIMessage also traverses each message's content, files, and attachments. Long conversations therefore incur O(total history) render work and allocations for every streamed update, rather than remapping only the changed tail, which can visibly degrade token streaming. Cache conversions by TMessage reference or select the mapped messages incrementally.

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 b49de6e. Views are cached in a WeakMap keyed by the stored TMessage, so a streamed cache write remaps only the messages it replaced. Covered by 'keeps unchanged message views across a streamed update', which asserts the untouched view is the same object.

@berry-13
berry-13 added this pull request to stack #16380 September 25, 2026 19:12
useChat reads ChatContext and presents it as @ai-sdk/react@4.0.117 does:
UI-shaped messages, a submitted/streaming/ready/error status derived
from the in-flight flag and the latest message, and sendMessage,
regenerate, stop and setMessages forwarding to the contract. It holds
no state and changes no existing consumer.
The parts mapping reads only the markers stored on a tool call; the facade supplies getToolMeta as its resolveToolFailure, so memory failure prose and background task status attachments reach the UI parts view the same way they reach the tool cards.
…er message

messages re-reads getMessages whenever the message query cache is written, through useSyncExternalStore, so setMessages and other cache writes show without an unrelated context update. Views are cached per message reference, so a streamed chunk remaps only the messages it replaced. status ignores placeholder parts through isEmptyContentPart, the error reads an Assistants error part's text, and a message inserted through setMessages joins the active conversation under the message before it.
@berry-13
berry-13 force-pushed the berry-13/chat-facade branch from 6a8a16b to b49de6e Compare September 25, 2026 21:45
@github-actions

Copy link
Copy Markdown
Contributor

Lighthouse CI failed. The last 80 log lines contain the measured budgets and assertion failures.

│ 21      │ 'http://localhost:3080/api/convos?pinned=true&limit=100'                                                        │ 2640.9069999999774 │ 3406.45199999999   │ 200    │
│ 22      │ 'http://localhost:3080/api/mcp/servers'                                                                         │ 2984.310999999987  │ 4258.296000000002  │ 200    │
│ 23      │ 'http://localhost:3080/api/permissions/mcpServer/effective/all'                                                 │ 2985.2489999999816 │ 3746.451999999961  │ 200    │
│ 24      │ 'http://localhost:3080/api/prompts/groups?limit=10'                                                             │ 2985.4559999999765 │ 4257.794999999984  │ 200    │
│ 25      │ 'http://localhost:3080/api/keys?name=openAI'                                                                    │ 3241.439999999973  │ 3897.8179999999993 │ 200    │
│ 26      │ 'http://localhost:3080/api/presets'                                                                             │ 3241.728999999992  │ 3908.9529999999795 │ 200    │
│ 27      │ 'http://localhost:3080/api/tags'                                                                                │ 3241.938999999984  │ 3911.557999999961  │ 200    │
│ 28      │ 'http://localhost:3080/api/share/link/16390000-0000-4000-8000-000000000001'                                     │ 3242.158999999985  │ 4258.076999999961  │ 200    │
│ 29      │ 'http://localhost:3080/api/messages/16390000-0000-4000-8000-000000000001'                                       │ 3243.5659999999625 │ 4404.347999999998  │ 200    │
│ 30      │ 'http://localhost:3080/api/files/config'                                                                        │ 3243.780999999959  │ 4163.524999999965  │ 200    │
│ 31      │ 'http://localhost:3080/api/agents/tools/web_search/auth'                                                        │ 3244.7089999999735 │ 6929.230999999971  │ 200    │
│ 32      │ 'http://localhost:3080/api/endpoints/token-config'                                                              │ 3245.076999999961  │ 4421.838999999978  │ 200    │
│ 33      │ 'http://localhost:3080/api/agents/tools/calls?conversationId=16390000-0000-4000-8000-000000000001'              │ 3245.460999999952  │ 4764.247999999963  │ 200    │
│ 34      │ 'http://localhost:3080/api/agents/chat/status/16390000-0000-4000-8000-000000000001?generationProtocolVersion=2' │ 4506.587999999989  │ 4762.343999999983  │ 200    │
└─────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────┴────────────────────┴────────┘

Inspect .lighthouse HTML/JSON and e2e/lighthouse/README.md. Reuse loaded user/config data; overlap independent reads without bypassing authorization.

┌─────────┬────────────────────────────┬─────────────────────┬───────┐
│ (index) │ audit                      │ median              │ limit │
├─────────┼────────────────────────────┼─────────────────────┼───────┤
│ 0       │ 'largest-contentful-paint' │ 4530.468            │ 4500  │
│ 1       │ 'cumulative-layout-shift'  │ 0.01844808311808656 │ 0.1   │
│ 2       │ 'total-blocking-time'      │ 274.9749999999708   │ 500   │
└─────────┴────────────────────────────┴─────────────────────┴───────┘

  1) [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

    Error: Median largest-contentful-paint must stay within 4500

    expect(received).toBeLessThanOrEqual(expected)

    Expected: <= 4500
    Received:    4530.468

       at audit.ts:159

      157 |   console.table(measured);
      158 |   for (const { audit, median, limit } of measured) {
    > 159 |     expect(median, `Median ${audit} must stay within ${limit}`).toBeLessThanOrEqual(limit);
          |                                                                 ^
      160 |   }
      161 |   return results;
      162 | }
        at auditPage (/home/runner/work/LibreChat/LibreChat/e2e/lighthouse/audit.ts:159:65)
        at /home/runner/work/LibreChat/LibreChat/e2e/lighthouse/load.spec.ts:33:19

    attachment #1: screenshot (image/png) ──────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/test-failed-1.png
    ────────────────────────────────────────────────────────────────────────────────────────────────

    Error Context: e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/error-context.md

    attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip
    Usage:

        npx playwright show-trace e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip

    ────────────────────────────────────────────────────────────────────────────────────────────────


🤖: global teardown has been started
2026-09-25 21:49:16 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-25 21:49:16 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ✅  Found user in Database
🤖:  ✅  Deleted 1 convos & 2 messages
🤖:  ✅  Deleted user from Database
🤖: global teardown has been started
2026-09-25 21:49:16 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-25 21:49:16 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ⚠️  User not found in Database
  1 failed
    [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

Open the full run

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

ℹ️ 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
Comment on lines +71 to +74
let view = views.get(message);
if (!view) {
view = toUIMessage(message, mappingOptions);
views.set(message, view);

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 Invalidate cached views for mutable stream messages

On the second and later content frame for a response, this returns the first cached UIMessage instead of remapping its updated content. Fresh evidence against the prior caching resolution is in client/src/hooks/SSE/useContentHandler.ts:55-82: messageMap deliberately reuses the same response object and mutates response.content for each frame, so message identity does not change as the comment assumes. Consequently, consumers of this facade can remain stuck on the first streamed chunk until the final server message replaces the object; cache against a content/version identity or make the stream producer immutable.

AGENTS.md reference: AGENTS.md:L31-L33

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 dc26613. Confirmed against useContentHandler: the response object is reused across frames and its content array replaced. The view cache now checks the fields a view reads (content, text, files, attachments, error, unfinished), not only the object, so a frame that replaces content remaps that response and unchanged messages keep their views. Covered by 'remaps a response whose content the stream replaced in place'.

Comment thread client/src/hooks/Chat/facade.ts Outdated
byToolCall = mapAttachments(message.attachments ?? []);
attachmentsByMessage.set(message, byToolCall);
}
const meta = getToolMeta({ type: ContentTypes.TOOL_CALL, tool_call: toolCall }, byToolCall);

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 tool attachments by sibling step

When a message contains repeated provider tool-call IDs and the newer call has not received a stepId yet, calling getToolMeta without its sibling-step set lets filterAttachmentsForPart associate an older step's attachment with the new call. This is the exact repeated-ID case handled by summarizeSpan in outcome.ts: an earlier memory-error artifact or failed/cancelled background-status attachment can make the newer call appear as output-error even though it is still running. Derive the owned sibling step IDs from the message content and pass them as getToolMeta's third argument.

AGENTS.md reference: AGENTS.md:L31-L33

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 dc26613. The resolver builds, per message snapshot, the step ids each provider tool-call id already owns, and passes them to getToolMeta as siblingStepIds for a call without a stepId, the way summarizeSpan does. Covered by 'keeps a stepless call clear of attachments its repeated id owns elsewhere', which fails without the fix.

…cade

The content handler reuses a response object across frames and replaces its content, so views are cached per message and per snapshot of the fields a view reads rather than by the object alone. The tool outcome resolver passes getToolMeta the step ids a repeated provider id already owns, so a call without a step is not failed by another step's attachment.

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

ℹ️ 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
Comment on lines +197 to +198
const readMessages = useCallback(() => getMessages(), [getMessages]);
const stored = useSyncExternalStore(subscribe, readMessages, readMessages);

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 Version snapshots for in-place stream writes

On the second and subsequent useContentHandler frame, client/src/hooks/SSE/useContentHandler.ts:79-97 replaces response.content on the same response object and writes an array containing the same message references. React Query's structural sharing can therefore retain the existing outer array, and although the subscription fires, readMessages() returns the identical snapshot, so useSyncExternalStore suppresses the render before isSameSource can inspect the changed content. Consumers remain on the first chunk until a later immutable or final write. Fresh evidence beyond the earlier view-cache fix is the unchanged outer snapshot identity at this external-store boundary; expose a per-write version/query state or make the stream write immutable.

AGENTS.md reference: AGENTS.md:L31-L33

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 2254d21. Confirmed: the content handler replaces content on the same response object, so structural sharing hands back the same cached array and the array-identity snapshot suppressed the render. The external-store snapshot is now keyed by the message cache write count (successful updates and removals of messages queries), so every frame re-renders and the per-field view cache remaps the response. Regression: 'follows each stream frame written to the cache with the same message references' in facade.spec.tsx runs a real QueryClient, asserts the cached array is retained, and fails on dc26613.

if (cached && isSameSource(cached, message)) {
return cached.view;
}
const view = toUIMessage(message, mappingOptions);

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 Handle empty tool-call slots before mapping

When a live/provider message contains an empty tool-call slot such as { type: 'tool_call' } or a null payload, this unconditional conversion throws because toToolPart passes undefined to readToolCall, which immediately reads .type. These slots are recognized transient input—isEmptyContentPart explicitly classifies tool calls missing their payload, while server compaction removes them only before persistence—so mounting the facade during such a stream can crash instead of returning an empty/step placeholder. Guard or normalize malformed tool calls before mapping, including in getToolContext.

AGENTS.md reference: AGENTS.md:L31-L33

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. The facade's own getToolContext loop now tolerates a missing tool_call (2254d21). The throw you traced sits in the base mapping (toToolPart/readToolCall, plus the agentId read in toUIMessage), which belongs to #16374 and is fixed there at 6a3685f with a parts.spec regression mapping the empty slot to step-start. It reaches this branch when #16374 merges and this stack rebases.

@danny-avila danny-avila added the 🗺️ Chat State Mgmt codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Sep 26, 2026
… in the facade

The content handler replaces a response's content on the same object, so
structural sharing keeps the cached message array and the external store saw
no change. The snapshot is now keyed by the message cache write count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw
@github-actions

Copy link
Copy Markdown
Contributor

Lighthouse CI failed. The last 80 log lines contain the measured budgets and assertion failures.

│ 21      │ 'http://localhost:3080/api/convos?pinned=true&limit=100'                                                        │ 2692.9689999999828 │ 3450.488000000012  │ 200    │
│ 22      │ 'http://localhost:3080/api/mcp/servers'                                                                         │ 3026.691999999981  │ 4289.937000000005  │ 200    │
│ 23      │ 'http://localhost:3080/api/permissions/mcpServer/effective/all'                                                 │ 3028.3669999999984 │ 3790.152999999991  │ 200    │
│ 24      │ 'http://localhost:3080/api/prompts/groups?limit=10'                                                             │ 3028.630999999994  │ 4294.669999999984  │ 200    │
│ 25      │ 'http://localhost:3080/api/keys?name=openAI'                                                                    │ 3292.825999999972  │ 3941.6749999999884 │ 200    │
│ 26      │ 'http://localhost:3080/api/presets'                                                                             │ 3294.63499999998   │ 3955.9069999999774 │ 200    │
│ 27      │ 'http://localhost:3080/api/tags'                                                                                │ 3294.880999999994  │ 3956.261999999988  │ 200    │
│ 28      │ 'http://localhost:3080/api/share/link/16390000-0000-4000-8000-000000000001'                                     │ 3295.3609999999753 │ 4297.222999999998  │ 200    │
│ 29      │ 'http://localhost:3080/api/messages/16390000-0000-4000-8000-000000000001'                                       │ 3295.7939999999944 │ 4449.457999999984  │ 200    │
│ 30      │ 'http://localhost:3080/api/files/config'                                                                        │ 3296.002999999968  │ 4211.467000000004  │ 200    │
│ 31      │ 'http://localhost:3080/api/agents/tools/web_search/auth'                                                        │ 3296.725000000006  │ 6981.173999999999  │ 200    │
│ 32      │ 'http://localhost:3080/api/endpoints/token-config'                                                              │ 3297.078999999998  │ 4469.43299999999   │ 200    │
│ 33      │ 'http://localhost:3080/api/agents/tools/calls?conversationId=16390000-0000-4000-8000-000000000001'              │ 3297.987999999983  │ 4800.892999999982  │ 200    │
│ 34      │ 'http://localhost:3080/api/agents/chat/status/16390000-0000-4000-8000-000000000001?generationProtocolVersion=2' │ 4563.841999999975  │ 4819.546000000002  │ 200    │
└─────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────┴────────────────────┴────────┘

Inspect .lighthouse HTML/JSON and e2e/lighthouse/README.md. Reuse loaded user/config data; overlap independent reads without bypassing authorization.

┌─────────┬────────────────────────────┬────────────────────┬───────┐
│ (index) │ audit                      │ median             │ limit │
├─────────┼────────────────────────────┼────────────────────┼───────┤
│ 0       │ 'largest-contentful-paint' │ 4528.432           │ 4500  │
│ 1       │ 'cumulative-layout-shift'  │ 0.0184480695283542 │ 0.1   │
│ 2       │ 'total-blocking-time'      │ 281.78999999999996 │ 500   │
└─────────┴────────────────────────────┴────────────────────┴───────┘

  1) [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

    Error: Median largest-contentful-paint must stay within 4500

    expect(received).toBeLessThanOrEqual(expected)

    Expected: <= 4500
    Received:    4528.432

       at audit.ts:159

      157 |   console.table(measured);
      158 |   for (const { audit, median, limit } of measured) {
    > 159 |     expect(median, `Median ${audit} must stay within ${limit}`).toBeLessThanOrEqual(limit);
          |                                                                 ^
      160 |   }
      161 |   return results;
      162 | }
        at auditPage (/home/runner/work/LibreChat/LibreChat/e2e/lighthouse/audit.ts:159:65)
        at /home/runner/work/LibreChat/LibreChat/e2e/lighthouse/load.spec.ts:33:19

    attachment #1: screenshot (image/png) ──────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/test-failed-1.png
    ────────────────────────────────────────────────────────────────────────────────────────────────

    Error Context: e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/error-context.md

    attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip
    Usage:

        npx playwright show-trace e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip

    ────────────────────────────────────────────────────────────────────────────────────────────────


🤖: global teardown has been started
2026-09-26 11:19:26 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-26 11:19:26 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ✅  Found user in Database
🤖:  ✅  Deleted 1 convos & 2 messages
🤖:  ✅  Deleted user from Database
🤖: global teardown has been started
2026-09-26 11:19:26 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-26 11:19:26 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ⚠️  User not found in Database
  1 failed
    [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

Open the full run

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