Skip to content

Merge upstream danny-avila/LibreChat @ v0.8.6 (756 commits) - #16

Merged
imryao merged 757 commits into
mainfrom
chore/merge-upstream-v0.8.6
Jun 14, 2026
Merged

imryao merged 757 commits into
mainfrom
chore/merge-upstream-v0.8.6

Conversation

@imryao

@imryao imryao commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Merges upstream LibreChat from current fork base (v0.8.3-rc1) up through v0.8.6.

What this PR does

Preserved fork-only changes

  • .gitlab-ci.yml
  • Custom endpoints in .env.example: GEMINI_VERTEX_ENABLED, GROQ/XAI/DEEPSEEK entries
  • Custom endpoint blocks in librechat.example.yaml
  • Branding: favicon, icons
  • Real-IP middleware: x-cs-client-ip header injection in OpenAI/Google/Anthropic endpoint initializers
  • 5 legacy updateUserKey('uid-${openidId}') blocks in openidStrategy.js (openAI/anthropic/google/Deepseek/xai) — to be removed in PR-2

Dropped from fork

  • UI "disable revoke / disable custom api key / remove set key button" tweaks — upstream UI rewritten extensively; may be reintroduced separately later

Incidental changes during merge

To get the test suite green:

  • api/jest.config.js + packages/api/jest.config.mjs: harmonized jest configuration (babel-jest transform, maxWorkers: '50%', setupFiles for packages/api, expanded esModules list to cover @langchain/mistralai and @mistralai/mistralai v2 which became ESM-only via the lockfile regen)
  • openidStrategy.spec.js: added updateUserKey: jest.fn() mock to cover the preserved legacy auto-import block
  • 3 endpoint initializers: changed fork's x-cs-client-ip block from req.headers['x-forwarded-for'] to req.headers?.['x-forwarded-for'] so unit tests omitting req.headers don't crash; behavior is identical when req.headers is populated (the only path used in production)
  • 1 lint fix in openidStrategy.js

What this PR does NOT do

  • Does not change runtime behavior for end users (OIDC users still get uid-${openidId} synthetic keys to the LLM gateway, same as before)
  • Does not migrate to OIDC access token forwarding (that is PR-2)
  • Does not restore the dropped UI key-locking tweaks

Developer notes

  • npm install now requires --legacy-peer-deps due to @tsdown/css vs rollup-plugin-postcss peer dependency conflict. CI's npm ci is unaffected (the resolved lockfile is committed).
  • Manual OIDC login smoke test deferred to staging (no local IdP configured during the merge). Staging validation required before merging this PR.

Verification

  • npm run lint — passes for fork-touched files; remaining errors are upstream baseline (~79 errors pre-existing on upstream main)
  • npm run test:api — 2864/2911 pass; 3 failing suites are pre-existing upstream issues (brotli/winston-daily-rotate-file infra)
  • npm run test:packages:api — 5885/5895 pass; 2 failing suites are pre-existing (missing optional dep, needs experimental-vm-modules)
  • npm run test:packages:data-provider — 1212/1213 pass
  • npm run test:packages:data-schemas — 1616/1616 pass
  • npm run test:client — 2526/2526 pass
  • Boot smoke: server listens on :3080, /health returns 200
  • Staging deployment with OIDC login + chat round-trip required before merge (rollout stage 2 in spec §8)

Rollout

See docs/superpowers/specs/2026-06-13-oidc-llm-forwarding-design.md §8 for the staged rollout plan.

🤖 Generated with Claude Code

danny-avila and others added 30 commits May 19, 2026 21:58
* feat: support data retention for normal chats

Add retentionMode config variable supporting "all" and "temporary" values.
When "all" is set, data retention applies to all chats, not just temporary ones.
Adds isTemporary field to conversations for proper filtering.

Adapted to new TS method files in packages/data-schemas since upstream
moved models out of api/models/.

Based on LibreChat-AI#10532

Co-Authored-By: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com>
(cherry picked from commit 30109e9)

* feat: extend data retention to files, tool calls, and shared links

Add expiredAt field and TTL indexes to file, toolCall, and share schemas.
Set expiredAt on tool calls, shared links, and file uploads when
retentionMode is "all" or chat is temporary.

(cherry picked from commit 4897375)

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

* fix: lint/test

(cherry picked from commit 310c514)

* fix: address code review feedback for data retention PR

Critical:
- Fix BookmarkMenu crash: restore optional chaining on conversation
- Fix migration hazard: backward-compatible sidebar filter that also
  checks expiredAt for documents without isTemporary field

Major:
- Add logging to getRetentionExpiry error path, align with tools.js
- Add tests for retentionMode: ALL in saveConvo and saveMessage
- Fix share route: apply expiredAt for temporary chats too by
  querying the conversation's isTemporary flag server-side
- Add assertions for getRetentionExpiry mocks in process tests

Minor:
- Fix ChatRoute isTemporaryChat to be strictly boolean via Boolean()
- Fix stale test description (expired -> temporary)
- Comment out retentionMode default in example yaml
- Simplify verbose if/else to isTemporary === true
- Add compound index on { user: 1, isTemporary: 1 }
- Remove narrating comment from process.spec.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6bad535)

* chore: fix typescript

(cherry picked from commit 826527a)

* fix: lint

(cherry picked from commit 77817e8)

* fix: use mockSanitizeArtifactPath in retention test

The 'getRetentionExpiry is called with the request object' test
referenced an undefined `mockSanitizeFilename` identifier, breaking
both lint (no-undef) and the test suite. Use the existing
`mockSanitizeArtifactPath` mock that the surrounding tests already
use, since `processCodeOutput` calls `sanitizeArtifactPath` (not
`sanitizeFilename`) before invoking `getRetentionExpiry`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 52ea2da)

* fix: forward isTemporary from client for retention on file uploads and tool calls

Server-side `getRetentionExpiry` (file uploads) and the tool-call
controller both read `req.body.isTemporary`, but the file upload
multipart form and the tool-call payload did not include that field.
In `retentionMode: temporary` (default), files uploaded and tool
calls created from temporary chats were therefore retained
indefinitely.

Forward the Recoil `isTemporary` flag in both client paths so the
existing server checks can fire correctly. `ToolParams` gains an
optional `isTemporary` field.

Addresses Codex P1 review feedback on PR LibreChat-AI#29.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7e937df)

* test: stub store.isTemporary in useFileHandling test mocks

Previous commit added `useRecoilValue(store.isTemporary)` to the
hook. The test file mocks `~/store` with only `ephemeralAgentByConvoId`
and does not stub `useRecoilValue`, so all 7 cases threw
"Invalid argument to useRecoilValue: expected an atom or selector but
got undefined". Add a stub default export with `isTemporary` and a
`useRecoilValue` mock returning `false`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit eb16095)

* fix: harden data retention semantics

* fix: provide sweep request context for expired files

* fix: preserve temporary flags in all-retention updates

* fix: honor assistant versions in retention sweeps

* fix: retain non-temporary flags in all mode

* fix: hide expired retained records

* fix: propagate retained conversation expiry

* fix: refresh meili retention cutoff

* fix: prevent overlapping file sweeps

* fix: show legacy retained conversations

* fix: index legacy retained records

* fix: harden retention cleanup edge cases

* fix: count failed file storage sweeps

* fix: preserve legacy temporary retention

* fix: assign retention sweep worker deterministically

* fix: hide expired shared links on reads

* fix: prevent retention refresh after parent expiry

* fix: break code output retention import cycle

* fix: harden retention review findings

* fix: ignore expired share duplicates

* fix: reject expired retained share creation

* fix: harden retention review edge cases

* fix: address retention audit findings

* fix: enforce expired conversation shares in all retention

* fix: scope temporary upload flag to chat files

* fix: address retention review findings

* fix: address codex retention review findings

* fix: tighten missing storage detection

* test: remove unused file process spec bindings

---------

Co-authored-by: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com>
Co-authored-by: Aron Gates <aron@muonspace.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🪂 feat: Graceful HTTP shutdown on SIGTERM/SIGINT

* Address feedback

* don't treat ERR_SERVER_NOT_RUNNING as fatal; route telemetry shutdown through coordinator
* fix: allow disabling file log transports

* fix: defer log directory setup when file logging disabled
* fix: Add admin panel URL Helm configuration

* fix: Clarify admin panel URL configuration

* fix: Avoid duplicate admin panel URL env
…hat-AI#13219)

* fix: implement response size limits for streamable HTTP MCP responses

- Added environment variables for maximum response and line sizes in streamable HTTP responses.
- Introduced functions to handle response size validation and error logging.
- Updated MCP connection logic to enforce these limits, ensuring safe handling of large responses.

* fix: address MCP response guard review findings

* fix: satisfy logger spy typings

* fix: clarify blocked MCP response errors

* fix: harden MCP guard review edge cases

* test: cover MCP oversized SSE call failures
* fix: improve OpenID lookup planning

* fix: add issuer-bound source id index

* fix: align OpenID source id index

* fix: preserve admin refresh recency
* feat: add Gemini 3.5 Flash support

* fix: refine Gemini 3.5 Flash overrides

* fix: satisfy Gemini thinking config types

* fix: drop empty Gemini thinking config
* fix: support DocumentDB prompt group lookup

* test: address prompt group review feedback

* test: prove prompt group indexed lookup
* feat: add MCP remote proxy support

* fix: Harden MCP Proxy Review Findings

* fix: Honor MCP Proxy Env Precedence

* fix: Harden MCP proxy routing

* fix: Align MCP proxy bypass semantics

* test: Pin MCP proxy admin scope
…AI#13239)

There are two ways to add a file to a conversation:

1. Uploading a new file.
2. Using an existing file (from the side panel).

If you decide to remove the file, the behavior differs depending on
how it was added. If you just uploaded a new file, it gets deleted
from the conversation & the system. But if it's an existing file,
then it only gets removed from the conversation (but not deleted).

However, in both cases, it would show a toast saying that the file
was deleted, which is incorrect for the "existing file" case.

Now we check whether the file is `attached` (to the system) before
showing the deletion toast, and skip showing it if we're not actually
deleting the file.
…Chat-AI#13242)

* chore: bump `@librechat/agents` to v3.1.90

* chore: npm audit fix

* chore: bump turbo
* fix: Harden MCP OAuth request handling

* fix: Bound MCP OAuth dispatcher cache

* fix: Harden OAuth DNS lookup handling
* 🦉 feat: Implement proactive OAuth flow for connections without stored tokens

* 🤝 fix: Enhance proactive OAuth flow handling in MCPConnectionFactory

* fix: Add timeout handling for proactive OAuth flow in MCPConnectionFactory

Co-authored-by: Copilot <copilot@github.com>

* fix: Refine proactive MCP OAuth flow

* test: Cover proactive OAuth missing handler

* fix: Require explicit MCP OAuth signal

* fix: Consolidate MCP OAuth policy

---------

Co-authored-by: Gil Assunção <gil.assuncao@parceiros.nos.pt>
Co-authored-by: Copilot <copilot@github.com>
* fix: prevent MCP server name collisions

* chore: address MCP registry review nits

* fix: reserve MCP config names from request context

* chore: format MCP registry changes

* chore: address MCP collision review findings
* add operational prometheus metrics

* fix metrics review findings

* fix metrics edge cases

* gate mongoose metrics instrumentation

* gate metrics setup when unconfigured
…ibreChat-AI#13248)

MCP OAuth access tokens are stored with a 365-day default expiry when the
provider's token response omits `expires_in` (only RECOMMENDED per RFC 6749
§5.1). Providers that issue short-lived JWT access tokens but omit
`expires_in` (e.g. Salesforce) therefore get tokens treated as valid for a
year and never refreshed, so every call 401s once the real token lapses
until the user manually reconnects.

When the access token is a JWT (RFC 9068), read its `exp` claim and use it as
the authoritative expiry, falling back to the 365-day default only for opaque
tokens. Explicit `expires_at`/`expires_in` still take precedence.

Adds unit tests for storeTokens expiry resolution.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix: allow OpenID PKCE authentication without client secret

* Linting

* Strategy fix

* fix(openid): trim secret gates and add PKCE client metadata tests

* chore(openid): normalize spec line endings

* ⚡ perf: Short-Circuit Config Override Resolution for Empty Principals (LibreChat-AI#12549)

Skip the getApplicableConfigs DB query when buildPrincipals returns
an empty array, since there are no principals to match against.

* ⚡ perf: Separate Error Handling for Principal Resolution vs Config Overrides (LibreChat-AI#12550)

Distinguish between buildPrincipals and getApplicableConfigs failures
so the uncached fallback to baseConfig is intentional and logged
separately from config override errors.

* Revert "⚡ perf: Separate Error Handling for Principal Resolution vs Config Overrides (LibreChat-AI#12550)"

This reverts commit 1729378.

* Revert "⚡ perf: Short-Circuit Config Override Resolution for Empty Principals (LibreChat-AI#12549)"

This reverts commit a100aa5.

---------

Co-authored-by: CMF\e-leite <EduardoLeite@criticalmanufacturing.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
…#13119)

* fix: add explicit skills/new route

* style: fix test formatting

---------

Co-authored-by: parkjingyeong <sand1166@hyundai.com>
* fix: Allow empty client_secret for MCP OAuth configuration

* fix: Enhance OAuth client registration logic to support predefined client_id and handle empty client_secret
* fix(mcp): detect non-standard OAuth errors from servers returning HTTP 400

* add tests for oauth error check

* fix(mcp): align factory OAuth error detection
…Chat-AI#13152)

* fix: skip MCP tools when required customUserVars are unset (LibreChat-AI#10969)

* fix: whitespace-only values

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: guard MCP registry lookup and unknown server config in customUserVars gate

* fix: fail closed on MCP registry lookup errors

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🏗️ refactor: Derive App Version from Root package.json + Add buildInfo Schema

The hardcoded `Constants.VERSION` in `data-provider` is now replaced at
rollup build time via `@rollup/plugin-replace`, sourcing from the root
`package.json` so version bumps are a single-file change.

Adds the shape needed by the rest of the series:
- `interface.buildInfo` boolean flag (default `true`) — lets self-hosters
  opt out of exposing commit/branch/date.
- `buildInfo` on `TStartupConfig` — commit/commitShort/branch/buildDate.
- `SettingsTabValues.ABOUT` — new settings tab enum value.

Ref: LibreChat-AI#12406

* 🛠️ feat: Add Build Metadata Resolver and Expose via /api/config

Adds `resolveBuildInfo()` in `@librechat/api` that surfaces commit SHA,
branch, and build date from (in order) `BUILD_*` env vars, then local git
metadata. Result is cached per-process.

`/api/config` includes a `buildInfo` field on both authenticated and
anonymous responses when `interface.buildInfo !== false` and at least one
resolver field is populated. Omitted entirely otherwise.

Designed so pre-built Docker images carry metadata via build-arg while
source installs pick it up from `.git` — no manual version tracking.

Ref: LibreChat-AI#12406

* ℹ️ feat: Add Settings → About Panel with Diagnostics Copy

New Settings tab that renders the running build's version, commit (short
SHA), branch, and build date in a monospaced block alongside a "Copy
diagnostics" button that emits a preformatted text blob for pasting into
support issues.

Tab is hidden when `interface.buildInfo` is set to `false`. Reads from
`startupConfig.buildInfo` provided by `/api/config`.

Ref: LibreChat-AI#12406

* 🐳 ci: Inject BUILD_COMMIT/BRANCH/DATE into Docker Images

Adds optional `BUILD_COMMIT`, `BUILD_BRANCH`, `BUILD_DATE` ARGs to both
`Dockerfile` and `Dockerfile.multi`, wired as `ENV` vars in the runtime
stage so the backend's `resolveBuildInfo` picks them up.

All image-publishing workflows (`tag`, `main`, `dev`, `dev-branch`,
`dev-staging`) now compute `${github.sha}`, `${github.ref_name}`, and a
UTC timestamp, then pass them to `docker/build-push-action` as
`build-args`.

Defaults are empty — non-CI builds (local `docker build`) still work,
and the backend falls back to local `.git` metadata if ARGs aren't set.

Ref: LibreChat-AI#12406

* 📝 docs: Direct Bug Reporters to Settings → About for Version Info

The previous instructions (`docker images | grep librechat`,
`git rev-parse HEAD`) only worked for a subset of deployments and
rarely produced a commit SHA for users pulling pre-built images.

Point users to the new in-app Settings → About panel's
"Copy diagnostics" button, which captures version, commit, branch,
build date, and user agent in a single preformatted block. Fallback
instructions preserved for older installs.

Ref: LibreChat-AI#12406

* 🐳 fix: Move BUILD_* ENV to End of Docker Stages to Preserve Layer Cache

Per-commit BUILD_COMMIT/BUILD_DATE changes were being promoted to ENV
before `npm ci` / `npm run frontend` (single-stage) and before
`npm ci --omit=dev` (multi-stage api-build), which invalidated the cache
for every subsequent layer on every CI run.

Move the ARG/ENV block below the heavy install and build steps in both
Dockerfiles. Metadata is still available in the runtime image but no
longer busts layer reuse.

Addresses codex review on LibreChat-AI#12756.

* 🔧 fix: Propagate interface.buildInfo=false to Unauthenticated /api/config

The unauthenticated branch of `/api/config` was emitting an `interface`
object only when `privacyPolicy` or `termsOfService` was set, which
meant an admin's explicit `interface.buildInfo: false` opt-out was never
visible to anonymous/guest clients. `Settings.tsx` gates the About tab
on `startupConfig?.interface?.buildInfo !== false`, so a missing field
fell through as "enabled" for those clients.

Include `interface.buildInfo: false` in the unauth payload whenever it's
explicitly disabled. Keep the implicit default (true) absent to preserve
the minimal-unauth-payload convention.

Addresses codex review on LibreChat-AI#12756.

* 🔀 ci: Trigger Dev Image Workflows on Root package.json + Dockerfile Changes

The baked `Constants.VERSION` now reads from the root `package.json` via
rollup-plugin-replace, but the `dev-images.yml` and `dev-branch-images.yml`
path filters only matched `api/**`, `client/**`, `packages/**`. A release
commit that only bumps root `package.json` would not trigger a rebuild,
leaving `latest` dev images with stale Footer/About version metadata.

Include `package.json`, `package-lock.json`, and both Dockerfiles in the
path filters so dependency changes (lockfile rebuilds) and image build
tweaks also rebuild dev images.

Addresses codex review on LibreChat-AI#12756.

* 🧽 fix: Harden About Panel Lifecycle, A11y, and Loading Gate

Review follow-ups on LibreChat-AI#12756:

- #1 timer leak: stash the copy-state `setTimeout` in a ref and clear it
  from a `useEffect` cleanup so unmounting the Settings dialog mid-toast
  doesn't fire `setCopied(false)` on an unmounted component.
- #3 flash of About tab: gate `aboutEnabled` on `startupConfig != null`
  so the tab stays hidden until `/api/config` returns. For admins who
  disabled `interface.buildInfo`, the tab no longer briefly appears and
  vanishes on page load.
- #6 aria-live placement: move the live region off the interactive
  button onto a dedicated `<span role="status" aria-live="polite">` so
  screen readers announce the copied state, not the full button content
  on every re-render.
- #2 missing coverage: add `About.spec.tsx` exercising populated/empty
  buildInfo rendering, invalid-date handling, diagnostics clipboard
  payload, copy-state toggling, unmount cleanup, and the live region.

* ⚡ perf: Eagerly Resolve Build Info at Module Load

Review follow-up #4 on LibreChat-AI#12756: `resolveBuildInfo()` calls `execFileSync`
with a 2s timeout on source installs without `BUILD_*` env vars. Paying
this cost on the first HTTP request blocks the event loop mid-flight.

Call `resolveBuildInfo()` once at config route module load so the
resolver's cache is warm before any request arrives. Docker images with
the BUILD_* env vars set sidestep the git path entirely, so this only
affects the edge case of source installs.

* 📝 docs: Document rollup Version Placeholder Contract

Review follow-ups #5 / #8 on LibreChat-AI#12756. The `__LIBRECHAT_VERSION__`
placeholder relies on a substring replacement rule that only works
because the token appears inside a string literal, and the substitution
only runs during `npm run build:data-provider`.

- Expand the `Constants.VERSION` JSDoc to spell out that consumers read
  the placeholder through the built dist bundle; source-level test
  imports would see the raw placeholder.
- Add a NOTE above the rollup `replace` config warning future
  contributors not to repurpose the token as a bare identifier without
  switching to a quoted replacement value.

Non-functional; prevents future contributors from stepping on a subtle
constraint.

* 🪪 fix: Only Toast "Copied" When Clipboard Copy Actually Succeeds

Codex R5 on LibreChat-AI#12756. `copy-to-clipboard` returns a boolean indicating
whether the underlying `execCommand('copy')` / fallback prompt actually
wrote to the clipboard. The previous handler flipped to the "Copied"
state unconditionally, which in hardened browsers or when the
permission prompt is dismissed would mislead users into filing bug
reports without the diagnostics blob attached.

Gate the state/timer/live-region on the boolean return; silently no-op
on failure rather than showing a false positive. Adds a test asserting
the button label stays at "Copy diagnostics" when the clipboard call
fails.

* 🐳 fix: Derive main image metadata from checkout

* 🪪 fix: Keep About enabled until disabled

* ✅ test: Avoid literal Settings mock text

* 🧱 refactor: Rename Build Info Module
danny-avila and others added 25 commits June 10, 2026 09:07
* 🧭 fix: Mobile Sidebar Navigation on Projects View

* 🧭 fix: Align Sidebar Toggle with JS Mobile Breakpoint at 768px
…eChat-AI#13652)

* 🗜️ ci: Cache Dependencies and Builds in Cache Integration Tests

Port the node_modules and package-dist caching pattern from
backend-review.yml to cache-integration-tests.yml, which ran a full
npm ci (~72s) and rebuilt data-provider, data-schemas, and api on
every run. Cache keys are identical to backend-review.yml so the two
workflows share entries. Drops setup-node's npm tarball cache,
superseded by the node_modules restore, matching backend-review.yml.

* 🗜️ ci: Exercise Warm-Cache Path
* perf(client): lazy-load query devtools

* fix: keep query devtools deps lazy

* fix: address query devtools review findings

* fix: exclude query devtools from pwa precache

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
…#13369)

* 🤫 fix: Silent MCP OAuth Refresh on Mid-Session 401

Avoids the hourly interactive re-auth prompt when an MCP server
(e.g. Azure Entra ID) returns 401 mid-session by attempting a refresh
token exchange first, and only falling back to the interactive OAuth
flow when no refresh token is stored or the refresh server rejects it.

Resolves LibreChat-AI#13364.

* fix: Use distinct flow type for silent token refresh to avoid cache hit

Addresses the Codex review on PR LibreChat-AI#13369: `attemptSilentTokenRefresh` was
reusing the `'mcp_get_tokens'` flow type, so
`FlowStateManager.createFlowWithHandler` would short-circuit and return
the same tokens cached by an earlier `getOAuthTokens` call — the very
tokens the server just rejected — without executing the forced-refresh
handler.

Switch silent refresh to the distinct `'mcp_force_refresh_tokens'` flow
type so coalescing still works but stale `mcp_get_tokens` cache entries
are not reused. After a successful refresh, invalidate the
`mcp_get_tokens` flow cache so the next `getOAuthTokens` call reads the
freshly persisted tokens from storage rather than the stale cached
value.

Add a regression test that simulates the real
`FlowStateManager.createFlowWithHandler` cache-hit behavior for
`mcp_get_tokens` and verifies the silent refresh handler still runs and
returns the freshly refreshed tokens.

* fix: Address Codex round-2 review on silent MCP OAuth refresh

Three follow-up findings from Codex on PR LibreChat-AI#13369:

1. The new `mcp_force_refresh_tokens` flow type was itself cached by
   `FlowStateManager.createFlowWithHandler`, so a subsequent 401 within
   the refreshed token's `expires_at` could re-serve the just-rejected
   token without ever re-running the refresh handler.

2. The factory's `oauthRequired` listener was removed immediately after
   the initial `attemptToConnect` succeeded, so a real mid-session 401
   emitted by `MCPConnection.connectClient` during transport recovery
   had no listener — the OAuth handled-promise would simply time out
   instead of triggering the silent refresh.

3. Routing the silent refresh through a distinct flow type broke
   coalescing with the `mcp_get_tokens` lock used by `getOAuthTokens`,
   letting two paths concurrently redeem the same stored refresh token.
   For providers that rotate refresh tokens (e.g. Azure Entra) the
   second redemption is rejected, kicking the user back into interactive
   OAuth despite a successful refresh elsewhere.

Resolution:

- Drop `FlowStateManager` from the silent-refresh path entirely. Replace
  with a process-local `inflightSilentRefreshes` Map keyed by
  `userId:serverName` that holds only the in-flight Promise (no cached
  result), so every fresh 401 after settlement triggers a fresh
  redemption while concurrent 401s for the same user/server still share
  one redemption.
- Stop calling `cleanupOAuthHandlers()` on successful initial connect,
  keeping the OAuth handler attached for the connection's lifetime so
  mid-session 401s actually reach `attemptSilentTokenRefresh`.
- Add a regression test reproducing the stale-cache scenario by faking
  the `mcp_get_tokens` cache hit and asserting silent refresh still runs
  against storage and returns the fresh tokens.
- Add a coalescing test asserting two concurrent oauthRequired events
  for the same user/server result in a single `forceRefreshTokens` call.
- Clear `inflightSilentRefreshes` in `beforeEach` to prevent
  cross-test leakage; switch the silent-refresh test mocks to
  `mockResolvedValueOnce` / `mockImplementationOnce` so leftover mock
  state cannot leak into later test cases.

Acknowledged remaining gap: the silent refresh still races
`getOAuthTokens`'s `mcp_get_tokens` flow when both run concurrently
(narrow window when an existing connection's local `expires_at` is
still valid but the server invalidated the token, and a new connection
is being created in parallel). The race is self-healing on the next
401 and documented inline.

* fix: Address Codex round-3 review on silent MCP OAuth refresh

Three more findings from Codex on PR LibreChat-AI#13369:

1. The in-flight silent-refresh promise was unbounded. If
   `forceRefreshTokens()` ever hung (slow provider, dropped TCP), the
   `inflightSilentRefreshes` lock stayed occupied forever and every
   later 401 for the same user/server joined the stuck promise instead
   of starting a fresh attempt or falling back to interactive OAuth.

2. The interactive-OAuth fallback didn't invalidate the
   `mcp_get_tokens` flow cache after persisting fresh tokens. For
   providers that don't issue refresh tokens (so silent refresh
   returns null), the old cache could still feed stale access tokens
   to the next `getOAuthTokens` call until its TTL expired — causing
   an immediate reconnect with the same just-rejected token.

3. When silent refresh failed, the handler fell through to
   `handleOAuthRequired()` whose recent-completion fast path can
   reuse a COMPLETED `mcp_oauth` flow within `PENDING_STALE_MS`. Those
   cached tokens are exactly the ones the server just rejected, so
   the connection would keep adopting them and looping on 401s until
   the cache aged out.

Resolution:

- Wrap `runSilentRefresh()` with a 60-second `withTimeout` (well under
  `connectClient`'s 120s OAuth timeout). On timeout the `.catch`
  resolves to null and the `finally` clears the in-flight entry, so
  the next 401 starts fresh and falls through to interactive OAuth.
- Extract two helpers — `invalidateGetTokensFlow` and
  `invalidateCompletedOAuthFlow` — and call them from the right
  branches: clear `mcp_get_tokens` after silent-refresh success AND
  after interactive-OAuth `storeTokens`; clear the COMPLETED
  `mcp_oauth` state (plus its CSRF mapping) before falling through to
  interactive OAuth so the fast-reuse path can't re-serve the
  rejected tokens.
- Add three regression tests: hung refresh release-the-lock under
  fake timers, completed-OAuth cache invalidation pre-fallback, and
  `mcp_get_tokens` invalidation after interactive token store.

* fix: Address Codex round-4 review on silent MCP OAuth refresh

Three more findings from Codex on PR LibreChat-AI#13369:

1. (P1) The silent-refresh in-flight lock keyed only by
   `userId:serverName`. In multi-tenant setups where two tenants share a
   userId (e.g. username-based IDs) and the same MCP server name, a
   concurrent mid-session 401 from tenant B would join tenant A's
   in-flight refresh and adopt tenant A's freshly minted tokens onto a
   tenant-B connection — a cross-tenant credential leak.

2. (P2) `invalidateGetTokensFlow` deleted the `mcp_get_tokens` flow
   state regardless of its status. When another connection was
   currently in `getOAuthTokens()` (PENDING flow) and joiners were
   monitoring it, the unconditional delete made those waiters see
   "Flow state not found" and unnecessarily fall back to interactive
   OAuth — even though fresh tokens were already being written.

3. (P2) The 60s `withTimeout` wrapping `runSilentRefresh()` only races
   the promise; it does not cancel the underlying `forceRefreshTokens`
   /  refresh-token HTTP request. If the request returned after a
   subsequent interactive OAuth had stored newer tokens, the late
   completion would `storeTokens` over the newer state. This requires
   a provider that doesn't rotate refresh tokens AND a refresh slower
   than 60s AND a successful interactive OAuth in that window — narrow
   but real.

Resolution:

- Capture `getTenantId()` into a new `factory.tenantId` field at
  factory construction time (before the OAuth handler closes over it
  outside the original request's async context) and include it in the
  silent-refresh lock key as `tenantId:userId:serverName`.
- `invalidateGetTokensFlow` now calls `getFlowState` first and only
  deletes when `status === 'COMPLETED'`. PENDING lookups are left
  alone so concurrent `getOAuthTokens` waiters via `monitorFlow` can
  still settle.
- For (3), document the race as a known limitation inline. Fully
  closing it requires threading an `AbortSignal` through
  `MCPTokenStorage.forceRefreshTokens` and the OAuth refresh handler
  to skip the late `storeTokens` after timeout — out of scope for this
  PR's surgical change.
- Add `getTenantId` to the `MCPOAuthConnectionEvents` test's
  `@librechat/data-schemas` mock so the factory constructor doesn't
  blow up under that suite.
- Add three regression tests: per-tenant lock isolation, PENDING-state
  preservation under `invalidateGetTokensFlow`, and (reused) the
  existing interactive-store invalidation test now driven through
  `getFlowState` returning the COMPLETED state.

* fix: Address silent MCP OAuth refresh review

Restore captured tenant context around token storage and OAuth fallback paths so mid-session callbacks do not lose tenant scope.

Thread AbortSignal through forced refresh and OAuth token requests, cap silent refresh by the connection OAuth timeout, and prevent timed-out refreshes from writing stale credentials after fallback.

Complete pending mcp_get_tokens flows with fresh tokens, add missing FlowState createdAt test fixtures, and cover the new tenant/abort/cache behaviors.

* fix: Tighten tenant-scoped MCP token refresh

Cap silent refresh by both the factory connect timeout and the connection OAuth wait timeout so fallback OAuth wins before the outer connect attempt expires.

Tenant-scope mcp_get_tokens flow ids for both token lookup and refresh invalidation, preventing cross-tenant flow completion or cache deletion when tenants share user ids and server names.

Add regression tests for the omitted initTimeout budget and tenant-prefixed token flow locks.

* fix: Reserve MCP OAuth fallback budget

* fix: Harden MCP OAuth refresh races

* fix: Keep MCP OAuth fallback route-compatible

* test: Add SDK MCP OAuth refresh repro

* fix: Address MCP OAuth refresh review findings

* fix: Address MCP OAuth tenant review findings

* fix: Close MCP OAuth route tenant gaps

* fix: Preserve MCP OAuth refresh flow guards

* fix: Avoid reprocessing MCP OAuth reauth config

* fix: Release timed-out MCP refresh locks

* fix: Release MCP OAuth request callbacks

* fix: Tenant-scope remaining MCP OAuth flow lookups

* ci: Sort imports in MCP OAuth test suites
…ibreChat-AI#13655)

* 📐 fix: Sidebar Chat List Width Tracking and Stale Row Measurements

* ✅ test: Sidebar Chat List Width Tracking e2e Coverage

* 🩹 fix: Address Review — Shrinkable List Wrapper, Seeded Measure, Fallback Resize

* ✅ test: Scope Sidebar Grid Selector and Cover Height Shrink

* 🧪 test: Settle Sidebar Sizes Before Asserting to Deflake CI
…at-AI#13658)

* ⏫ ci: Bump GitNexus to 1.6.7 to Fix Embeddings Index Timeout

* ⏲️ ci: Raise GitNexus Index Timeout for 1.6.x Embedding Volume
* 🔐 fix: Resolve template vars and respect custom Authorization on model fetch

The custom-endpoint model fetch path in `fetchModels` had two bugs that
silently broke per-user authentication on `GET /v1/models`:

1. Template variables in the configured `headers:` block were not
   substituted on the OpenAI-compatible branch. Only the Ollama branch ran
   `resolveHeaders`, so placeholders like `{{LIBRECHAT_OPENID_ID_TOKEN}}`
   were forwarded as literal strings on every other endpoint.
2. After spreading the (unresolved) headers into the request, the code
   unconditionally executed
   `options.headers.Authorization = \`Bearer ${apiKey}\`` and clobbered any
   `Authorization` the operator had set in `headers:`.

Combined, these meant a config like
```yaml
endpoints:
  custom:
    - name: "MyProxy"
      apiKey: "${MY_API_KEY}"
      headers:
        authorization: "Bearer {{LIBRECHAT_OPENID_ID_TOKEN}}"
```
sent `Authorization: Bearer ${MY_API_KEY}` on `/v1/models` instead of the
user's resolved JWT — even with `OPENID_REUSE_TOKENS=true` set. Auth-aware
proxies (e.g. LiteLLM with team-based JWT auth) therefore could not return
a per-user filtered model list.

This change runs `headers` through `resolveHeaders` (mirroring the Ollama
branch) and only falls back to the apiKey-based default when the resolved
headers do not already supply an `Authorization` (case-insensitive). All
other endpoints behave unchanged: when no `Authorization` is configured,
the existing `Bearer ${apiKey}` default still applies.

Tests added:
- Template variables in custom headers are resolved on the OpenAI path.
- A config-supplied `Authorization` overrides the apiKey default.
- The override check is case-insensitive (`authorization` works too).

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

* 🔐 fix: Address review — import order, P1 token leak guard, P2 token-config path

- Fix sort-imports drift in `models.ts` and `custom/initialize.ts`.
- P1: in `loadConfigModels` (`config/models.ts`), do not forward
  `endpointHeaders` to `fetchModels` when `baseURLIsUserProvided`.
  Configured templates such as `Authorization: Bearer
  {{LIBRECHAT_OPENID_ID_TOKEN}}` would otherwise resolve and be sent to a
  destination the user controls — leaking the user's identity token.
  Header overrides remain in place when only the apiKey is user-provided
  (admin-trusted base URL).
- P2: in `initializeCustom` (`custom/initialize.ts`), the token-config
  fetch path now forwards `headers` and `userObject` to `fetchModels`
  (mirroring the auth-aware behaviour), with the same `userProvidesURL`
  guard. Additionally, when `endpointConfig.headers` is set the model
  cache is skipped to avoid a per-user filtered response leaking across
  users; token-config caching was already user-keyed when key/URL are
  user-provided.

Tests added:
- `config/models.spec.ts` (new): verifies the P1 guard — headers are
  dropped when the base URL is user-provided, and forwarded when only the
  apiKey is user-provided.
- `custom/initialize.spec.ts`: three cases for the P2 path covering header
  forwarding to admin-trusted base URLs, header drop on user-provided
  base URLs, and absence of `skipCache` when no headers are configured.

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

* 🔐 fix: Scope model + token-config caches when user-bound headers are forwarded

Two follow-up fixes from the second review pass:

P1.1 (`fetchModels` / `models.ts`): the MODEL_QUERIES cache is keyed by
baseURL+apiKey only. When callers forward headers containing template
variables that resolve against the current user (e.g. `Authorization:
Bearer {{LIBRECHAT_OPENID_ID_TOKEN}}`), one user's filtered list could be
served to the next request that happens to share the same baseURL+apiKey.
`shouldCache` now skips the cache whenever both `headers` and `userObject`
are supplied — that's the unambiguous signal the response is being
resolved against a specific user identity. Existing callers that pass
neither (fetchOpenAIModels, fetchAnthropicModels) keep their cache.

P1.2 (`initializeCustom` / `custom/initialize.ts`): the surrounding
tokenConfigCache uses `tokenKey === endpoint` when key+URL are
admin-configured. With user-bound headers forwarded, the first user's
token config could be cached for the shared endpoint and served to other
users until TTL. `tokenKey` is now also user-scoped when
`endpointConfig.headers` will be forwarded (i.e. base URL is
admin-trusted, so the security guard leaves headers in place).

Also removed the explicit `skipCache: !!endpointConfig.headers` from the
fetchModels call in initializeCustom — the new fetchModels-level rule
covers it uniformly across both call sites.

Tests added:
- models.spec.ts: cache skipped on `headers + userObject`; cache used
  when only one of them is supplied (existing callers unaffected).
- initialize.spec.ts: `tokenKey` is `${endpoint}:${userId}` when headers
  will be forwarded, and `endpoint` (unscoped) when no headers are
  configured.

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

* 🔐 fix: Include header fingerprint in in-request model fetch coalescing key

`loadConfigModels` coalesces concurrent fetches for endpoints that share
the same admin-trusted `${BASE_URL}__${API_KEY}` via `fetchPromisesMap`.
With per-endpoint `headers:` overrides — including templates that resolve
against the current user — that key is too coarse: two custom endpoints
sharing a proxy URL/key but configuring different headers (e.g. distinct
`X-Tenant` values, or different static `Authorization` strings) would
share a single fetch promise, and the first endpoint's filtered response
would be returned for the second endpoint within the same request.

Fix: include a stable SHA-256 fingerprint of the configured headers in
the coalescing key. Endpoints that genuinely share `baseURL + apiKey +
headers` still share one fetch (preserves the existing optimisation);
endpoints that differ in headers each get their own fetch.

Test added in `config/models.spec.ts`:
- Two endpoints sharing baseURL+apiKey but with different headers result
  in two `fetchModels` calls, each carrying the right headers.
- Two endpoints sharing baseURL+apiKey AND identical headers still
  coalesce into a single `fetchModels` call.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iews (LibreChat-AI#13654)

* 🚪 fix: Align Mobile Sidebar Toggle Gating with JS Breakpoint Across Views

* 🚪 fix: Sidebar Toggle on Read-Only Prompt Details View
…ibreChat-AI#13664)

The as-any casts existed only to reach the protected Keyv cache and
private localSnapshotExpiry members. TypeScript's element-access escape
hatch provides the same access fully typed, so the casts and their
eslint-disable directives are unnecessary. The directives also reported
as unused under configs that relax no-explicit-any for test files.
…t-AI#13662)

Adds an opt-in showOnLanding flag to model specs. When set, the chat
landing shows the spec's label and description in place of the
time-of-day greeting; specs without the flag are unaffected, so existing
deployments see no behavior change. HTML-valued descriptions (inline
icons + markup) render sanitized via the shared config-HTML sanitizer
with a new media tag/attribute allowlist, both on the landing and in
model selector items. Excludes e2e specs from the typed client lint
block so staged e2e files no longer fail pre-commit with 'file not
found in project'.
* feat: Add GitHub skill sync

* fix: Address GitHub skill sync CI

* fix: Harden GitHub skill sync review paths

* fix: Prevent overlapping skill sync runs

* fix: Address GitHub skill sync review findings

* fix: Satisfy Git ref lint rule

* fix: Address GitHub sync review follow-ups

* fix: Match skill frontmatter closing fence

* fix: Address GitHub sync review cycle

* fix: Address GitHub sync review follow-ups

* fix: Harden GitHub skill sync worker

* fix: Format GitHub sync rollback log

* fix: Address GitHub sync review feedback

* fix: Format skill import parse handling

* fix: Coerce scalar skill frontmatter and correct scheduler timer clear

- parse: coerce numeric/boolean name and description scalars to strings instead of dropping them to empty (restores pre-refactor behavior; preserves absent-vs-empty distinction for the when-to-use fallback)
- scheduler: clear the setTimeout handle with clearTimeout rather than clearInterval
- test: cover non-string scalar frontmatter coercion

* fix: Tolerate trailing whitespace after SKILL.md opening frontmatter fence

extractFrontmatterBlock required the opening fence to be exactly '---\n', so an opener with trailing spaces/tabs (e.g. '---   \n') silently dropped all frontmatter even though the closing-fence regex already tolerates it. Match the opener with /^---[ \t]*\n/ for symmetry. Addresses Codex P3 (parse.ts:24).

* feat: Run GitHub skill sync under a per-source tenant context

Under TENANT_ISOLATION_STRICT, the sync ran with no async tenant context, so the tenant-isolation mongoose hooks threw on every Skill/SkillFile/AclEntry operation; in non-strict mode synced skills were written tenant-less and never matched tenant-scoped reads. Add an optional per-source tenantId to the skillSync config; when set, each source sync runs inside tenantStorage.run({ tenantId }) so skills, files, and public ACL grants are created and listed within that tenant, and the skill row is stamped with the tenantId for correct dedup. Sources without tenantId keep the prior single-tenant behavior. Avoids runAsSystem. Addresses Codex P2 (sync.js:70).

Lock/status/credential bookkeeping stays outside the tenant context (those collections are intentionally global).

* test: Restore dropped tenant-context coverage for GitHub skill sync

The prior commit shipped the getTenantId import in github.spec.ts without the tenant tests that use it (lost in an interrupted edit), which failed the eslint --max-warnings=0 CI job on an unused import. Restore both github.spec.ts tenant tests (tenant-scoped run stamps tenantId and executes inside the tenant ALS context; no-tenant run stays ambient) and the two config-schemas tenant tests (accepts tenantId, rejects __SYSTEM__).

* test: Restore dropped github.spec tenant-context tests

The previous commit's github.spec.ts edit did not apply (anchor mismatch), so the getTenantId import remained unused and failed eslint --max-warnings=0. Add the two tenant tests that use it: a tenant-scoped run stamps tenantId and executes inside the tenant ALS context, and a no-tenant run stays ambient.

* feat: Scope synced skill author to tenant and harden tenant-context sync

Addresses the latest Codex review on the per-source tenant change:
- makeSourceAuthorId now folds tenantId into the synthetic author hash so the
  same source mirrored into different tenants gets distinct author ids (clearer
  audits, no cross-tenant author collisions). Single-tenant author ids stay
  stable (suffix omitted when tenantId is absent).
- syncSourceInTenantContext uses an async callback per the tenant-context
  contract so the ALS store propagates across awaited Mongoose calls.
- Tests: same-source/different-tenant yields distinct authors; mirror cleanup
  is scoped to the source and deletes only its absent-upstream skills.

* fix: Repair tsc error and guard external edits in github skill sync

- Fix TS2352 in github.spec mirror-cleanup test: build the existing-skill mock via makeSkill with authorName instead of an under-typed 'as CreateSkillInput' cast (this was the failing TypeScript CI check on f00ce3c5a).
- 808: commitExistingRemoteSkillAfterFileSync re-reads to clear our own file-sync version bumps, but now compares refreshed content against the pre-sync snapshot (body/name/description/always-apply) and throws SKILL_CONFLICT on a concurrent external edit instead of overwriting it.

* docs: Note skillSync source tenantId is effectively immutable

Changing/adding/removing a source's tenantId orphans previously mirrored skills in the old tenant (a tenant-scoped sync cannot clean another tenant's data without runAsSystem, which is intentionally avoided).

* fix: Key GitHub skill upstream identity on source id and path only

Addresses Codex finding (github.ts:217): makeUpstreamId previously included owner/repo, so repointing a source to a renamed or replacement repository (same source id) changed the upstreamId, made findSkillBySourceIdentity miss the existing mirror, and then collided on the (name, author, tenantId) uniqueness constraint — leaving the source stuck failing. Identity now keys on the stable source id + root path only. The feature is unreleased, so there is no stored-id migration. Updated spec upstreamId fixtures to the new format; the existing ref-independent identity test now also covers repo moves.

* fix: Scope GitHub skill mirror deletion to the source tenant

Addresses Codex P1 (github.ts:1047/1057): an ambient source (no tenantId) runs listSkillsBySource without tenant context, which under non-strict isolation returns github-synced skills across all tenants. The mirror-deletion pass then treated other tenants' skills as absent-upstream and could delete them. Filter existingSyncedSkills to rows whose tenantId matches the source's configured tenantId (absent = its own ambient bucket) before deleting, so a sync never removes another tenant's mirrored skills. Covered by a test where an ambient run leaves a tenant-b-owned skill untouched.

* fix: Apply tenant-scoped mirror deletion implementation

The prior commit (75ccfa3fc) added the test but the source change to github.ts was lost in an interrupted edit, leaving a failing test with no implementation. This adds the actual guard: the mirror-deletion pass skips skills whose tenantId does not match the source's configured tenantId (absent = ambient bucket), so an ambient source whose listSkillsBySource returns cross-tenant rows under non-strict isolation cannot delete another tenant's mirrored skills.

* fix: Resolve global access role outside tenant context for synced skill grants

Addresses Codex P2 (github.ts:1166): default access roles (incl. skill_viewer) are seeded globally with no tenantId under runAsSystem, but a tenant-scoped sync wraps ensurePublicViewer in the source's tenant context. The PermissionService grantPermission resolved the role via a tenant-isolated AccessRole query, so the global role did not match and tenant-scoped syncs failed with 'Role skill_viewer not found'. The sync adapter now resolves the role inside runAsSystem (matching the global seed) and writes the ACL entry in the active tenant context, so the AclEntry is tenant-scoped (visible to tenant users) while the role lookup still succeeds. Covered by service tests for the resolve-vs-write split and the missing-role failure.

* fix: Strip placeholder frontmatter booleans and check skill conflict before file sync

- 1083 (github.ts:759): toCleanFrontmatter now drops a non-boolean always-apply (e.g. the 'always-apply:' / 'always-apply: # TODO' placeholder, which js-yaml yields as null). The boolean is already captured in the dedicated alwaysApply field; persisting null left ambiguous frontmatter on the synced skill.
- 1080 (github.ts:1057): for an existing mirrored skill, check for an external content edit (via getSkillById + hasExternalSkillEdit) BEFORE syncSkillFiles mutates the bundled files, so a concurrently edited skill fails fast with SKILL_CONFLICT without partial file rewrites. The post-file-sync check still guards edits that land during the file sync window.
Tests: placeholder always-apply is dropped from synced frontmatter; concurrent-edit conflict leaves files unmutated (no upsert/delete).

* fix: Harden GitHub skill sync review paths

* fix: Reuse moved GitHub skill mirrors

* fix: Scope GitHub sync identity conflicts

* test: Fix GitHub sync conflict mock typing

* fix: Support nested env-backed skill sync

* fix: Keep skill sync config base-only

* fix: Scope GitHub skill identity lookup by tenant

* fix: Harden GitHub skill sync admin gates

* fix: Guard existing skill sync permission grants

* feat: Trigger skill sync from resolved config

* fix: Scope resolved skill sync by tenant

* test: Allow manual skill sync status tenant scoping

* refactor: Extract skill sync trigger orchestrator

* test: Complete orchestrator status fixture

* chore: Bump data provider version

* fix: Restrict skill sync server credentials

* test: Complete admin skill sync status fixtures

* fix: tighten skill sync trigger safeguards

* fix: preserve alwaysApply skill sync alias

* chore: sort skill sync imports

* fix: preserve skill sync request scope

* fix: harden skill sync review edges

* refactor: move skill sync admin access to api package

* fix: add skill sync declaration return types

* fix: satisfy skill sync type checks

* fix: resolve codex skill sync review findings

* fix: harden skill sync review edges

* fix: resolve codex skill sync edge findings

* fix: satisfy API declaration build after rebase
The gitnexus droplet is ~8.7GB usable, not the 60GB the disk-cleanup
comment assumed. With /usr (~2.8GB), the in-use docker images (~2.1GB),
and the growing /opt/gitnexus/indexes (~1.2GB), deploys were aborting at
the `AVAIL_MB < 2048` guard ("Disk critically low").

Two fixes:
- Reclaim the previous gitnexus image after force-recreate. The pre-pull
  `docker system prune -af` cannot remove it while the old container is
  still running, so a stale ~700MB generation accumulated every deploy.
  A post-recreate `docker image prune -f` makes the box self-cleaning.
- Lower the abort threshold 2048 -> 1536MB. The image is ~700MB and
  shares most layers with the running one, so an incremental pull needs
  well under 1GB; the old guard was sized for the 60GB assumption.

Also corrects the stale 60GB comment to reflect the actual disk.
…I#13669)

* fix(mcp): preserve request-scoped tools for PTC execution

* fix(mcp): preserve run-scoped tools on initialized agents
* fix(mcp): reuse request-scoped connections per run

* test(mcp): update connection factory defaults
A Codex security finding flagged that findPiiMatchInMessages was
gating on msg.role === 'user' and silently skipping every other
role. The OpenAI-compatible validator accepts system, assistant, and
tool from the caller; the Responses input conversion accepts and
converts developer and system. All of those roles flow into
formatAgentMessages and then createRun, so an authenticated remote
agent caller could place a credential-shaped value in any non-user
role and reach the model despite the configured filter.

Drops the role gate. The helper now scans every caller-supplied
message regardless of role; the loop count is unchanged (one outer
over messages, one inner over content parts) and the early-exit on
first match still holds. Spec adds explicit cases for system,
assistant, and tool roles in place of the now-incorrect
skips-non-user assertion.
…ibreChat-AI#13686)

* 🛟 fix: Auto-Recover from Stale Service Worker Assets After Deploys

- 404 missing static assets in the SPA fallback instead of serving index.html
- inline recovery script unregisters stale SWs and reloads once on chunk failure
- route vite:preloadError into the same recovery path for stale lazy chunks

* 🛟 fix: Address Review — SW-Side Recovery, Scoped Unregister, Shared Fallback

- importScripts'd sw-heal.js pings window clients on activation and reloads
  ones that can't pong: stale pages carry no recovery code of their own
- scope SW unregistration to the deployment base for subpath installs
- preventDefault vite:preloadError only when a recovery reload was initiated
- extract createSpaFallback and apply the asset 404 guard to experimental.js
…Chat-AI#13697)

The no-spec branch of `useApplyModelSpecEffects` (added in LibreChat-AI#11796) reset
`ephemeralAgentByConvoId` to null on every `newConversation` call when
model specs are configured. On in-place model/endpoint switches (modular
chat, same conversation or new-chat draft), BadgeRowContext never refills
from localStorage — its init effect only re-runs when the storage suffix
or spec changes — so the MCP selection (and tool toggles) were silently
dropped from subsequent request payloads while the MCP badge kept
displaying them.

Reset now only happens on context transitions (leaving a spec, or moving
to a different conversation key), where a BadgeRowContext refill is
guaranteed; in-place non-spec switches preserve the ephemeral agent.

- Gate the no-spec reset on `prevSpecName` / `prevConvoId`, passed from
  `newConversation` via a snapshot read of the pre-switch conversation
- Add jest coverage for all five branches of the no-spec path
- Add e2e spec asserting `ephemeralAgent.mcp` stays in the chat payload
  after a new-chat model switch and after regenerate on a switched
  conversation (verified failing before the fix, passing after)
- Add non-spec "Mock Provider D" endpoint to the e2e config so tests can
  switch between two real ephemeral endpoints; widen `MockEndpoint` type
…t-AI#13698)

* 📬 feat: Report Tool Results Per Call via onResult Channel

Tool batches already execute in parallel here, but results were only
delivered to the agent graph through the single resolve(results[])
call — so a fast tool's completion event waited on the slowest call
in the batch. Report each result through the optional onResult channel
(agents SDK > 3.2.33) as it settles, letting the graph emit that
call's completion immediately. resolve remains the authoritative batch
outcome; the callback is optional-chained, so this is a no-op until
the SDK release lands and remains backward compatible after.

* 🧹 chore: Prettier Formatting in onResult Spec

* 🧹 chore: Sort Imports in handlers.ts

* 🔧 chore: Update @librechat/agents dependency to version 3.2.34 in package-lock.json and related package.json files
- Bump @grpc/grpc-js versions to 1.9.16 and 1.14.4, updating resolved URLs and integrity hashes.
- Add license information for the updated packages.
Preserved fork-only changes:
- .gitlab-ci.yml
- custom endpoints in .env.example and librechat.example.yaml: GEMINI_VERTEX_ENABLED, GROQ/XAI/DEEPSEEK entries
- branding: favicon, icons
- real-ip middleware: x-cs-client-ip header injection in OpenAI endpoint initializer

Temporarily preserved (to be removed in PR-2):
- openidStrategy: 5x updateUserKey('uid-${openidId}') auto-import for openAI/anthropic/google/Deepseek/xai endpoints

Dropped (UI divergence too high; may be reintroduced later):
- UI 'disable revoke / disable custom api key / remove set key button' tweaks

Notes for developers:
- npm install requires --legacy-peer-deps due to @tsdown/css vs rollup-plugin-postcss
  peer conflict (CI's npm ci is unaffected because the lockfile is resolved)
- Manual OIDC login smoke deferred to staging (no local IdP configured)
- jest config (api/jest.config.js, packages/api/jest.config.mjs) updated to
  transform @langchain/mistralai + @mistralai/mistralai (latter is now ESM-only
  at version 2.x as resolved by the regenerated lockfile)
- openidStrategy.spec.js mock updated with updateUserKey: jest.fn() to cover the
  preserved legacy auto-import block
- Fork-preserved x-cs-client-ip blocks in openai/google/anthropic initializers
  now use optional chaining (req.headers?.['x-forwarded-for']) so unit tests
  that omit req.headers don't crash

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@imryao
imryao merged commit 9bedc33 into main Jun 14, 2026
14 of 22 checks passed
@imryao
imryao deleted the chore/merge-upstream-v0.8.6 branch June 14, 2026 05:52
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.