Commit graph

4781 commits

Author SHA1 Message Date
Danny Avila
a53936d273
🧭 test: Cover Agent Handoffs End to End (#14428)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* test: cover agent handoffs end to end

* style: sort handoff imports

* fix: normalize missing agent handoff edges

* chore: update package dependencies and versions in package-lock.json and package.json

* chore: bump agents SDK
2026-07-27 08:47:15 -04:00
unsnow-iac
8374b8416a
📧 fix: Restore invite-user CLI (#14436)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix: restore invite-user CLI

* fix: guard createInvite failure shape in invite-user CLI

createInvite resolves to { message } on error rather than throwing, so the
CLI interpolated it into the register link as [object Object] and emailed
an unusable invite.

* fix: normalize invite email before token creation

findToken lowercases its email query but the Token schema has no lowercase
setter, so a mixed-case address passed to the CLI was stored verbatim and
the resulting invite could never be redeemed.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-07-26 23:35:50 -04:00
Danny Avila
d8427ffc5e
🛂 test: Cover Tool Approval Workflows End to End (#14427)
* test: cover tool approval workflows end to end

* fix: preserve tool approval state across resume

* fix: preserve agent context in mock stream responses

* fix: preserve nested approvals in collapsed groups
2026-07-26 21:58:25 -04:00
Danny Avila
f3159f9891
🧩 fix: Harden Agent Skill Lifecycles End to End (#14429)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
* test: cover agent skill lifecycles end to end

* style: sort agent skill imports
2026-07-25 08:19:12 -04:00
Danny Avila
73699b5c25
⚡ perf: Reduce Agent Chat Startup Latency (#14423)
* perf: reduce agent chat startup latency

* test: align Redis stream readiness assertions

* perf: overlap remaining agent startup work

* perf: persist initial agent job metadata atomically

* test: add agent startup latency benchmark

* fix: harden resumable agent stream lifecycle

* fix: isolate replacement stream lifecycles

* fix: preserve terminal stream epochs
2026-07-25 07:58:20 -04:00
Danny Avila
cd215150cc
✳️ feat: Claude Opus 5 Support (#14422)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* ✳️ feat: Claude Opus 5 Support

- Add claude-opus-5 to Anthropic/Bedrock model lists, token maps, and pricing
- Extend requiresExplicitThinkingDisabled to Opus 5 so thinking-off sticks
- Clamp xhigh/max effort to high when thinking is disabled (Opus 5 400)

* 🪣 fix: Use Bedrock Inference Profiles and Add Vertex Opus Models

Bare `anthropic.` Claude 4+ IDs are not invocable on-demand via Converse:
Bedrock rejects them with "Retry your request with the ID or ARN of an
inference profile that contains this model." Verified live against
us-west-2 for Fable 5, Opus 5, Opus 4.8, Sonnet 5, Sonnet 4.6, Opus 4.6,
Sonnet 4.5, Haiku 4.5, and Opus 4.1. Switch those defaults to the
`global.` profile (no regional pricing premium); Opus 4.1 has no global
profile, so it uses `us.`.

Also add the modern Opus family to the Vertex defaults. `loadEndpoints`
swaps the shared Anthropic list for the Vertex model names, so Opus was
invisible to every Vertex deployment that did not enumerate models by hand.

* 📋 chore: Cover Opus 5 Gaps From PR #14420

Picks up items from the parallel community PR by @jona7o:

- Add claude-opus-5 to the librechat.example.yaml Vertex example (both the
  legacy array and the deploymentName map), which already lists Fable 5
  and Opus 4.8
- Mention Opus 5 in the configureReasoning doc comment, and note that its
  early return is why the effort cap is enforced by the caller
- Assert Opus 5 carries no long-context premium pricing
- Cover the Sonnet 5 negative case for the effort cap, and the persisted
  disabled-object round-trip carrying an effort

* 🌍 docs: Warn That Vertex Regional Endpoints Reject Modern Models

Anthropic serves Sonnet 4.6 and earlier on specific Vertex regional
endpoints; newer models (Opus 4.7+, Opus 5, Sonnet 5, Fable 5) require
`global` or a multi-region location and 404 on a specific region. The
`us-east5` default therefore cannot serve the Opus models added here, nor
the Sonnet 5 entry that predates this branch.

Documents the constraint at all three places an operator sets the region,
and at the fallback itself. Leaves the default unchanged: switching it to
`global` would silently alter data routing and residency for existing
deployments, which is a separate call.

* 🩹 fix: Restore PDF Exemption for Undated IDs and Gate Vertex Defaults

Two issues raised in review:

- BEDROCK_CLAUDE_4_PLUS_RE required a `-` after the major version, so it
  matched `claude-opus-4-8` but not undated IDs like `claude-opus-5`.
  Those models silently lost the Claude 4+ PDF exemption and fell back to
  the 4.5 MB limit. Sonnet 5 and Fable 5 were already affected before this
  branch; Fable/Mythos were also missing from the family alternation.

- The Vertex defaults advertised models that only `global` and the
  multi-region locations serve, so a default `us-east5` deployment listed
  Opus choices that 404 on first request. Filter the built-in defaults by
  configured region instead of changing the region default, which would
  alter data routing for existing deployments. An explicit `vertex.models`
  list is the operator's choice and is never pruned.

* 🧩 fix: Match Bare Claude IDs in the Bedrock PDF Exemption

An application inference profile maps a LibreChat model ID with no
`anthropic.` segment, so `claude-opus-5` failed the Claude 4+ check and
fell back to the 4.5 MB PDF limit. Make the prefix optional and accept
both segment orders, mirroring BEDROCK_CLAUDE_4PLUS_THINKING in
librechat-data-provider, which matches on the family token for exactly
this reason.

Only reached for the Bedrock provider, so the looser prefix cannot leak
into other endpoints. Verified Claude 3.x, Nova, Llama, Cohere, and
Mistral IDs still fall through to the default limit.

* 🧹 fix: Drop Retired Claude 3.5 Models From Bedrock Defaults

The three Claude 3.5 entries reached end of life at AWS and return
ResourceNotFoundException in every prefix form (bare, `us.`, `global.` —
verified live against us-west-2), so selecting one was a hard error.
Their modern equivalents are already in the list: Sonnet 5 / Sonnet 4.6
supersede the 3.5 Sonnets, and Haiku 4.5 supersedes 3.5 Haiku.

Every remaining Anthropic default is now live-verified invocable.
`.env.example` swaps its retired example ID for Haiku 4.5.

* 🔒 refactor: Narrow Effort-Clamp Types Instead of Asserting

Both clamp sites reached into loosely-typed containers with assertions:
llm.ts used an `as unknown as { type?: string }` double assertion to read
the thinking type, and the Bedrock parser cast `output_config` to
`{ effort?: unknown }` before confirming it was an object. CLAUDE.md's
type-safety rules call for narrowing over both.

Adds `isThinkingDisabled` and `clampOutputConfigEffort` to
librechat-data-provider, using `in`-operator narrowing and a type
predicate so no assertion is needed at all. Both call sites now share one
implementation rather than duplicating the clamp.

Behavior is unchanged; existing clamp tests cover it.
2026-07-24 21:45:32 -04:00
Danny Avila
6c97a7f467
♾️ fix: Preserve Resumable Stream Ordering Across Turns (#14411)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
* fix: preserve resumable stream ordering across turns

* chore: sort stream regression imports

* test: mirror sliding sequence ttl in publisher mock

* fix: prevent duplicate early stream replay

* fix: preserve replay frontier when sync fails
2026-07-23 13:25:38 -04:00
Danny Avila
21dc4a2ef4
🎯 fix: Correct Off-by-One Rail Scrub After Pinning the Terminus (#14409)
Some checks failed
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Pinning the scroll-to-bottom rib moved it out of the column, but scrubTo
kept enumerating ribs from the nav (messages + terminus) while measuring
the fraction against the column, which now spans the messages alone. Every
drag position mapped one rib late: pointing at the middle of the rail
scrolled to the message below the rib under the cursor.

Enumerate the column's own ribs for the proportional mapping and reach the
terminus by dragging past the column's bottom edge, where it now sits.
2026-07-23 11:48:45 -04:00
Danny Avila
142973e7e8
🌍 i18n: Update translation.json with latest translations (#14406) 2026-07-23 10:03:12 -04:00
Danny Avila
60eba76375
🫙 fix: Preserve Loaded Message Content When Resume Snapshot Is Empty (#14399)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🛟 fix: Keep Loaded Message Content When a Resume Snapshot Is Empty

The sync handler guarded on `data.resumeState?.aggregatedContent` and then
assigned it unconditionally. An empty array is truthy, so a resume snapshot
carrying no content overwrote the content the messages query had already
loaded, leaving the message rendering as a bare cursor with no way to recover.

Treat an empty snapshot as non-authoritative: keep the loaded content and let
live deltas take over. A snapshot that carries content still wins, unchanged.

This is defense in depth rather than a root cause. A resume snapshot goes empty
when the conversation's job is replaced mid-flight (#14348) — because
`streamId === conversationId`, a second submission calls `createJob` on the same
key while the first run is still streaming. With this change that failure
degrades to stale-but-visible instead of destroying content already on screen.

* 🔒 fix: Scope Resume Content Preservation to Identity-Matched Responses

Codex P2: on a regenerate reload before the new run aggregated anything, the
server reports an empty snapshot under a response id not yet in the loaded
history. The parent-based fallback then lands on the answer being REPLACED, and
preserving its content seeded `syncStepMessage` with the stale response, so the
regenerated run's deltas appended to it instead of starting blank.

Preserve loaded content only when the row was matched by the server's declared
`responseMessageId` — the sole case proving the row belongs to this generation.
A fallback-matched row keeps the previous clear-on-empty behavior.

* 🧱 fix: Only Preserve Resume Content When the Row Actually Has Parts

A matched row with no `content` array would have been assigned `undefined`
instead of the snapshot's array. `MultiMessage` branches on `message.content`
truthiness to pick its renderer, and `[]` is truthy while `undefined` is not, so
that would have silently switched a streaming row from the content-parts
renderer to the text one.

Preserve only when the loaded row has a non-empty content array — the case the
guard exists for. Narrows the divergence from prior behavior further: it now
applies solely when there is real content to protect.
2026-07-23 08:22:17 -04:00
Danny Avila
30ae414911
📌 fix: Pin Scroll-to-Bottom Rib in Message Nav (#14397)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 📌 fix: Pin Scroll-to-Bottom Rib in Message Nav

Render the terminus rib outside the scrolling rail, between the column
and the down chevron, so the scroll-to-bottom affordance stays in view
no matter how far the rail has scrolled.

- scrubTo enumerates ribs from the nav so drag-to-bottom still lands on
  the terminus
- the pinned rib drives the shared preview itself on hover and focus,
  since it is no longer covered by the column's pointer magnification

* 🖱️ fix: Keep Drag-Scrub Startable From the Pinned Terminus

Pointer-down on the pinned rib no longer reaches the column's handler now
that it renders outside the scrollport, so wire the same drag-start to the
wrapper. Dragging up from the bottom dot scrubs the thread again.
2026-07-22 22:13:29 -04:00
Danny Avila
00c5a747e9
🧵 feat: Native Background Execution for Code Interpreter Tools (#14386)
* 🧵 feat: Native Background Execution for Code Interpreter Tools

* 🩹 fix: Address Codex Round 1 (fallback dedupe, harvest failure, handle parsing)

* 🩹 fix: Live Completion Marker + Unkeyed Attachment Dedupe (Codex Round 2)

* 🎨 chore: Sort Imports + Widen Marker Type Comparison (CI)

* 🩹 fix: Stale-Harvest Guard, Error Marker Status, Faster Anchor Retry (Codex Round 3)

* 🧹 refactor: TS Harvest Module, Claim-Neutral Timestamps, Error Parity (Codex Round 4)

* 🩹 fix: Dispatch-Ordered Stale Guard, Foreground Downgrade, Error Wrapper Parity (Codex Round 5)

* 🩹 fix: Retry Past Unfinished Rows + Per-Call Attachment Dedupe (Codex Round 6)

* 🩹 fix: Writer-Dispatch Ordering, Scoped Live Upserts, Reaped-Task Wrapper (Codex Round 7)

* 🩹 fix: Wildcard toolCallId Matching for Bare Attachment Updates (CI)

* 🩹 fix: Claim-Insert Dispatch Stamp (Schema-Backed) + Scoped Status Markers (Codex Round 8)

* 🩹 fix: Pre-Write Ownership CAS + Agent-Scoped Part Patching (Codex Round 9)

* 🩹 fix: Insert-Path Ownership CAS + Agent-Routed Attachments (Codex Round 10)

* 🩹 fix: Agent-Scoped Marker Ids and Attachment Dedupe (Codex Round 11)

* 🩹 fix: Atomic File Commit and Sibling Preview Fan-Out (Codex Round 12)

- Replace the two-step claim-confirm CAS with an atomic conditional updateFile: the ownership predicate (no sourceDispatchedAt, or <= this write's dispatch order) moves into the update filter, removing confirmCodeFileOwnership and the lost-update window between check and write
- Thread agentId through createDownloadFallback so fallback download rows scope to the emitting agent like primary rows
- Fan terminal preview overlays out to every live attachment sharing the file_id in useAttachmentPreviewSync (sibling tool calls no longer stick on pending)
- Restore background artifacts through toStoredArtifact so the size bound applies on re-anchor
- Apply filterAttachmentsForPart to grouped tool-call attachments in ContentParts so handoff agents with colliding provider call ids do not cross-contaminate groups

* 🩹 fix: Agent-Scoped Live Upserts and Monotonic Dispatch Stamps (Codex Round 13)

- Scope the SSE attachment upsert and the useAttachments DB/live merge by agentId with the same wildcard semantics as toolCallId: distinct non-null agentIds stay separate entries, so handoff agents sharing a claimed file_id and a repeated provider tool id (call_0) no longer merge over each other's cards
- Extend the attachment identity key to fileKey::toolCallId::agentId and register less-specific key variants so bare and agent-less live records still dedupe after overlay
- Stamp background task createdAt from a strictly-increasing per-process dispatch counter: raw Date.now() can tie for same-millisecond dispatches and the stale-output guard accepts equal stamps (needed for idempotent re-commits), which would let an older task overwrite a newer task's committed file
2026-07-22 22:13:15 -04:00
Danny Avila
5af12c722e
🎭 ci: Scope Playwright Runs to Relevant Changes (#14388)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-07-22 12:23:39 -04:00
Danny Avila
dbb771aa7c
🚦 ci: Gate Playwright Runs to Maintainers (#14385)
* ci: gate Playwright runs to maintainers

* ci: guard pull request context explicitly
2026-07-22 12:23:02 -04:00
Danny Avila
ad5bb477af
🎞️ fix: Surface Clear Error for Unprocessable Gemini YouTube Videos (#14396)
Google rejects a YouTube video it cannot ingest with a generic
`400 INVALID_ARGUMENT` that names no cause, which LibreChat relayed
verbatim. Attribute the failure using request context instead: when a
Google/Vertex turn carried an injected YouTube video part and the
provider returns that generic rejection, map it to a typed error the
client localizes.

Verified against the live API: a public 9h15m video is refused this way
on gemini-2.5-flash, 3.5-flash, 3.5-flash-lite and 3.6-flash, including
at MEDIA_RESOLUTION_LOW, while a short video with an identical payload
succeeds. Duration is the dominant trigger; region and access
restrictions return the same response, so the copy leads with length
without overclaiming.

A duration preflight was evaluated and skipped: oEmbed does not expose
duration, leaving only watch-page scraping — a blocking call against
undocumented markup from rate-limited datacenter IPs that would fail
open and still need this mapping underneath.
2026-07-22 12:11:06 -04:00
Danny Avila
337facb4f0
✋ fix: Enable Submit on First Tool-Approval Decision (#14393) 2026-07-22 12:09:53 -04:00
Danny Avila
4c0ac8844c
🪆 fix: Preserve Nested Subagent Delegation (#14392)
Co-authored-by: Sien Nuyens <sien.nuyens@ixor.be>
2026-07-22 09:30:53 -04:00
Danny Avila
af7b2761eb
🪟 perf: Virtualize Search Results and Stop the Per-Query Remount (#14352)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🪟 perf: Virtualize Search Results and Stop the Per-Query Remount

* 🔧 fix: Type-safe globalThis cast in Search route test

* 🔑 fix: Stabilize Search Row Keys and Harden Virtualized Result Edges

Address Codex review findings on the virtualized search results view:

- Key rows by messageId (outer React key + CellMeasurer + cache keyMapper all
  aligned) so React reconciles by message, not scroll slot.
- Compare title and conversationId in areSearchMessagePropsEqual so a rename or
  refetch that keeps text/id intact still re-renders the row.
- Recompute cached heights on same-length content changes (file previews
  resolving, refetch) and on font-size changes.
- Keep the aria-live announcement on the empty-results branch.
- Don't paginate the outgoing query while the user is still typing.
- Show the spinner during the initial debounce instead of a blank route.

* 🧵 fix: Reset Scroll, Remeasure Growth, and Footer-Pad Virtualized Search

Address the second Codex round on the virtualized search results view:

- Reset the List scroll to the top on a new query (results stay mounted via
  keepPreviousData, so the List otherwise keeps the previous scrollTop and can
  open a new search mid-list); a font-size change keeps the user's place.
- Re-measure a row when its content later grows/shrinks (tool/code output
  expands, a late image loads) via a ResizeObserver that clears just that row's
  cached height and recomputes from it.
- Give the load-more throttle trailing:false and cancel it when the query
  changes, so a queued fetch can't page a stale search.
- Add a fixed trailing spacer row so the last result clears the bottom
  gradient/spinner overlay.

* 🫥 fix: Gate Stale Search Results on Refetch State, Not Just Typing

Address the third Codex round: `isTyping` clears when the debounce publishes the
new query, but `keepPreviousData` keeps the old pages mounted until the new
request lands, leaving a window the typing-only guards missed.

- Derive `showingStale = isTyping || isPreviousData` and gate both the dimming
  and pagination on it, so the outgoing results stay dimmed and don't page while
  the new query is still fetching.
- Compare `unfinished` in areSearchMessagePropsEqual so a finish/cancel that
  changes only that flag re-renders SearchContent's incomplete-response notice.

* 📐 fix: Invalidate Row Height Against the Cache and Compare clientTimestamp

Address the fourth Codex round on virtualized search:

- Compare each ResizeObserver height against the cached row height instead of
  skipping the first callback, so a cached/fast-loading image that is already
  taller than CellMeasurer's mount measurement still invalidates the stale
  height (no more overlap/clipping of following rows).
- Compare clientTimestamp in areSearchMessagePropsEqual, since the row timestamp
  falls back to it when createdAt is absent.

* 🕳️ fix: Spinner Over False Nothing-Found for Stale Empty Search Data

Address the fifth Codex round: when the previous search had zero matches,
keepPreviousData holds those empty pages (isPreviousData, isLoading false)
during the new request, so the loading gate missed it and flashed a false
"nothing found". Gate the spinner on `showingStale` too, not just isLoading/
isTyping.
2026-07-22 04:26:47 -04:00
JOJO
8751cc1c5c
🔗 fix: Preserve resource owner access when sharing (key share diff by stable id) (#14317)
* 🔗 fix: Preserve resource owner access when sharing (key share diff by stable id)

The share dialog diff (GenericGrantAccessDialog.handleSave) keyed added/removed
principals by `idOnTheSource`, which is inconsistent for the same user across
sources: getResourcePermissions returns `userInfo.idOnTheSource || _id` (the
external oid for OpenID/Entra users) while the people-picker returns the local
`_id`. The resource owner then appears in both `updated` and `removed`, and — since
updateResourcePermissions applies grants (upsert) before revocations (delete) — the
owner's own ACL entry is deleted when they add anyone to the share list. They then
get 403 on GET/edit/re-sharing their own resource.

Extract the diff into a pure computeShareChanges() helper keyed by
`id ?? idOnTheSource` (stable local id when present, external oid fallback for
principals not yet synced locally, e.g. unsynced Entra groups/users). Add unit tests.

Not reproducible with local-only users, where idOnTheSource falls back to _id and
both sources agree.

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

* 🔧 fix: address review — dedupe share diff by principalKey; drop test assertion

- computeShareChanges now diffs over the de-duplicated map values, so a principal
  that appears more than once in the input (possible while the add/dedupe path still
  keys on idOnTheSource) is never emitted multiple times in updated/removed.
- Drop the `as TPrincipal` assertion in the test helper — the literal is structurally
  compatible with TPrincipal, so TypeScript validates the shape directly.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 04:21:28 -04:00
Danny Avila
ca6ffb33fd
📦 chore: Update @librechat/agents to v3.2.68 (#14380)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
2026-07-21 21:40:54 -04:00
Danny Avila
cbaa2fe2e3
⚡ feat: Add Gemini 3.6 Flash and Gemini 3.5 Flash-Lite Support (#14369)
* ⚡ feat: Add Gemini 3.6 Flash and Gemini 3.5 Flash-Lite Support

Adds first-class support for Google's Gemini 3.6 Flash (`gemini-3.6-flash`)
and Gemini 3.5 Flash-Lite (`gemini-3.5-flash-lite`) for both the Gemini API
(AI Studio) and Google Cloud/Vertex integrations.

- Context window (1M) in googleModels; API + cache pricing in tx.ts.
- Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations.
- Generalize the Gemini 3.5 Flash overrides into a flash-family handler that
  strips deprecated temperature/topP/topK and applies each model's default
  thinking level (3.6 Flash: medium, 3.5 Flash-Lite: minimal), with
  longest-prefix resolution so flash-lite does not collide with flash.

Ref: https://ai.google.dev/gemini-api/docs/latest-model#api-changes-and-parameter-updates

* 🩹 fix: Strip unsupported penalty params for Gemini Flash family

Gemini 3.6 Flash, 3.5 Flash-Lite, and 3.5 Flash reject presencePenalty/
frequencyPenalty with HTTP 400 ("Penalty is not enabled for this model",
verified live). These pass through llmConfig via knownGoogleParams, so add
them to the flash-family strip list alongside the deprecated sampling params.

* 🩹 fix: Strip Flash-blocked params on custom Google endpoint path

For custom OpenAI-compatible endpoints with defaultParamsEndpoint=google,
getOpenAIConfig strips Flash-blocked params via getGoogleConfig but then
transformToOpenAIConfig re-applies raw addParams, undoing the strip. Filter
addParams through stripGeminiFlashBlockedParams before the transform so the
deprecated sampling / rejected penalty params cannot reach the provider.

* 🔧 chore: Update sharp package to version 0.35.3 in package-lock.json, api/package.json, and packages/api/package.json

* 🔧 chore: Update dependencies in package-lock.json to latest versions for @google/genai (2.13.0), @hono/node-server (1.19.14), fast-uri (3.1.4), hono (4.12.31), and svgo (2.8.3)

* 🔧 chore: Update dependencies in package.json and package-lock.json for @librechat/agents (3.2.67), @opentelemetry/sdk-node (0.221.0), and add new dependencies for @opentelemetry/propagator-jaeger (2.10.0) and protobufjs (7.6.5). Update monaco-editor version in client package.json to 0.56.0.

* 🔧 chore: Upgrade turbo package to version 2.10.5 in package.json and package-lock.json, and update schema reference in turbo.json

* 🩹 fix: Resolve CI breakage from bundled dependency bumps

Not related to the Gemini models — both are fallout from the dep bumps on
this branch:
- monaco-editor 0.56 changed IEditorHoverOptions.enabled from boolean to
  'on' | 'off' | 'onKeyboardModifier'; update ArtifactCodeEditor to match
  (mirrors the sibling occurrencesHighlight/matchBrackets pattern).
- sharp 0.35.3 fails resize+encode on a degenerate 1x1 PNG (vipspng: libpng
  read error); the provider-file e2e fixture was 1x1, so use a 16x16 PNG.
  Normal images are unaffected (verified 64x64 resize/encode/jpeg all OK).

* 📝 docs: Correct e2e image-fixture comment (bad IDAT CRC, not a sharp bug)

Root cause was the old 1x1 fixture's corrupt IDAT CRC (verified: IHDR/IEND
CRC OK, IDAT CRC BAD), which sharp 0.35.3's stricter libpng correctly rejects.
Not a dimension/resize edge case and not a sharp bug; comment now reflects that.
2026-07-21 21:14:11 -04:00
Danny Avila
1dd7121d71
🌍 i18n: Update translation.json with latest translations (#14376) 2026-07-21 20:36:35 -04:00
Danny Avila
a3c92b83c8
🧾 ci: Skip Workflows for Markdown-Only Changes (#14378) 2026-07-21 20:35:59 -04:00
Danny Avila
deea679f3a
🤐 fix: Withhold MCP OAuth Headers From Untrusted Preconfigured Discovery (#14379) 2026-07-21 20:35:38 -04:00
Danny Avila
3f51fc5fbe
🧭 fix: Keep Message Nav Chevrons Working on In-Thread Steers (#14377)
An applied steer renders nested inside the response, whose `relative` content
column becomes the steer's offsetParent, so its `offsetTop` is local to that
column rather than measured against the scroll content like top-level rows. The
rail compared that value against `scrollTop`, so once the viewport reached a
steer every jump/current-row decision was computed in the wrong coordinate
space — the "previous" chevron kept re-targeting the steer and got stuck.

- Add `entryTop(el, container)` that sums `offsetTop` up the offsetParent chain
  until it leaves the scroll container, folding nested steers back into one
  content-space origin (top-level rows collapse to a single hop)
- Use it for the four message-entry measurements (current row, offset cache,
  jump previous/next); leave the column-rib offsets untouched
- Guard `getCurrentVisibleId` on a null scroll container
- Test a nested steer (local offset inside a positioned column) lands its rib at
  the true thread position; fails with the old single-hop offsetTop
2026-07-21 19:56:57 -04:00
Danny Avila
3e9f07976a
🧩 fix: Preserve Deployment Skill IDs on Agents (#14368)
* fix: preserve deployment skills on agents

* fix: expose deployment skills to agent viewers

* refactor: centralize deployment skill ID merging

---------

Co-authored-by: Dennis Schenk <dennis@gridonic.ch>
2026-07-21 19:44:27 -04:00
MarcAmick
ade02054c8
🛟 fix: Keep File Uploads Alive With SSE Heartbeats (#14295)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: Use SSE to upload files in order to avoid idle timeouts.  Idle timeouts can occur for example from gateways and other services like cloudfare when uploading large files.  For example during rag processing the file is uploaded to librechat which then sends it to rag.  While librechat is waiting for the embeddings to come back from rag the file upload is sitting idle.  Gateways tend to want to cancel the upload with an http 408 , 504, or 524.  This change uses SSE to perform the upload so that while librechat is sending the file to rag, it consistently sends back a heartbeat event to the client to keep the connection alive.  This is especially useful when utilizing  EMBEDDING_BATCH_SIZE in librechat rag which will allow rag to process signifigantly larger files without running out of memory.

* added tests to packages\api\src\files\sse.spec.ts in order to test the new sse.ts

* fix: Harden SSE file upload lifecycle

* style: Sort data provider imports

---------

Co-authored-by: Marc Amick <MarcAmick@jhu.edu>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-07-21 19:27:09 -04:00
Danny Avila
7406f5d79e
🌍 i18n: Update translation.json with latest translations (#14307) 2026-07-21 12:38:25 -04:00
Danny Avila
21766b5b3c
🧠 fix: Restore Agent Memory Scope Control in Unified Builder (#14292)
* 🧠 fix: Restore Agent Memory Scope Control in Unified Builder

The Agent Builder redesign (#13952) surfaced memory as a Tools marketplace
item and stopped rendering SidePanel/Agents/Memory.tsx, orphaning the file and
removing the 'Keep memories separate for this agent' control shipped in #14084.

Only the render was lost: the locale keys, memory_scope on AgentForm, the
AgentPanel save path, and AgentSelect hydration all survived, which is why
setting memory_scope directly on the agent document still worked.

Restore it as a builtin item setting (the seam the new builder uses for
per-tool config): mark memory configurable so its row gets a cog, and render a
MemoryConfig branch in BuiltinSection mirroring ArtifactsConfig. Delete the
orphaned component so there is a single source of truth.

Resolves #14287

* 🧹 chore: Remove Orphaned com_agents_enable_memory i18n Key

The key labeled the enable checkbox in the deleted SidePanel/Agents/Memory.tsx.
The unified builder labels the catalog item via com_ui_memory, so it has no
remaining consumer and detect-unused-i18n-keys fails on it.
2026-07-21 12:37:51 -04:00
Danny Avila
87b3557f11
🫧 feat: Float In-Flight Steers Over the Thread with Collapsible Content (#14365)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
- Float the in-flight steer stack as a bottom-anchored overlay (`absolute
  bottom-full`) so it no longer shrinks the message viewport and older messages
  scroll behind it; a per-conversation Jotai height atom reserves an equal band
  of bottom padding on the message scroll area so the newest message rests clear
- Collapse long steers to a ~128px preview with a bottom fade and a Show
  more/Show less toggle, shown only once the content overflows the cap
- Sticky the per-steer options menu (`sticky top-2`) so it stays reachable while
  scrolling through a tall, expanded steer
2026-07-21 11:43:06 -04:00
Danny Avila
913540d00a
📦 chore: Update @librechat/agents to v3.2.66 & npm audit fix (#14361)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 📦 chore: Update `@librechat/agents` to v3.2.66

* chore: npm audit fix
2026-07-21 08:44:00 -04:00
Danny Avila
ad46f66dc4
🧬 perf: Memoize Message Spine and Isolate Scroll-Button State (#14330)
* 🧬 perf: Memoize Message Spine and Isolate Scroll-Button State

* ✅ test: Pin the message-row memo comparators against field drift

areMessageFieldsEqual and areMessageRowPropsEqual gate every message row's
re-render but had no direct tests. Add a completeness suite: a field-mutation
table asserts each compared field flips the comparator to false (a dropped
field fails its case), plus same-ref / equal-distinct-objects / nullish cases,
and the same shape for the row-props comparator including its delegation into
areMessageFieldsEqual.
2026-07-21 08:35:33 -04:00
Danny Avila
71fa24a6ea
🎛️ perf: Narrow Composer Subscriptions to Streaming State (#14333)
* 🎛️ perf: Narrow Composer Subscriptions to Streaming State

* ✅ test: Cover Composer Subscription Refactor's Behavioral Changes

Add regression tests for the previously-uncovered changed behavior in the
composer-subscriptions refactor:

- useLatestMessageMeta: exact projected field set, null on empty cache, and
  referential stability + no re-render across token-only cache writes.
- useGetLatestMessage: call-time tail read, stable callback identity with no
  re-render on cache writes, Recoil-snapshot sibling-branch resolution, null
  with no conversation.
- useSubmitMessage: reads the tail at call time and appends it to root when
  missing (and does not when present or absent) — the reconcile branch the
  prior test skipped via an early return.
- useHandleKeyUp: ArrowUp in an empty composer clicks the latest message's edit
  control, with the null / missing-control / non-empty-composer guards.
- useAskAnswerMode (new spec): liveAsk is projected through the
  findLiveAskUserQuestion select, null when empty/disabled.

* 🎨 style: Fix import order in useLatestMessage spec
2026-07-21 08:28:38 -04:00
Danny Avila
9e245aced4
🎟️ fix: Claim Idempotency Keys to Dedup Retried Generation Requests and Prevent Double Billing (#14344)
* 🐛 fix: Dedup retried start-generation requests to prevent duplicate billing

A lost or reset start-generation response makes the client re-POST the
identical payload (up to 3x on network errors). The resumable-stream
controller had no idempotency: createJob unconditionally overwrote the
running job without aborting the prior one, so both requests ran full
LLM completions and both billed while the UI showed only one (#14339).

Add a stable per-submission clientRequestId (uuid, fresh per ask() so a
regenerate differs, reused across the start-generation retries) and an
atomic claim on the job store keyed by userId:clientRequestId. The first
request wins and generates; a retried POST loses the claim and receives
the original stream, which the client subscribes to and replays - no
second billed generation.

- IJobStore.claimIdempotencyKey/releaseIdempotencyKey (in-memory Map+TTL,
  Redis single-key SET NX PX + GET Lua, cluster-safe)
- GenerationJobManager.claimGeneration/releaseGeneration (20m TTL)
- Controller claims before the concurrency check, dedups with a resumed
  response, releases on start-failure/429
- clientRequestId threaded through TSubmission/TPayload/createPayload

* 🐛 fix: Harden start-generation dedup (Codex review)

Address three P2 findings on the idempotency path:

- Resume replay: a deduped retry now subscribes with resume=true so the
  client replays prior content and any pending-action from the running
  stream instead of only live events (cross-replica / HITL correctness).
  startGeneration returns { streamId, resumed } and the response's
  status:'resumed' drives the subscribe mode.
- Wait for the job record: a duplicate that loses the claim now waits
  briefly for the winner to create the job before returning the stream
  (a stream with no job 404s terminally). If the winner has not
  materialized, return 503 SERVER_NOT_READY so the client retries via the
  existing readiness path instead of attaching to a dead stream.
- Release only owned claims: track whether the request actually won the
  claim; the 429 and init-error paths no longer release a claim owned by
  another in-flight generation (fail-open path could erase it and
  re-enable double billing).

Adds controller tests covering dedup, the 503 race fallback, win-then-
create, and claim-release ownership on 429 / fail-open.

* 🐛 fix: Don't trap deduped retries on missing job records (Codex review)

The previous round returned 503 SERVER_NOT_READY when a deduped retry's
job record was absent. But a missing job usually means the original
generation already completed and was cleaned up (cleanupOnComplete) — the
correct recovery is to return the stream and let the client's subscribe
404 handler refetch the persisted messages. The 503 instead trapped the
send in a readiness-retry loop until the client's window expired.

Keep the bounded wait (it still covers the job-about-to-be-created race)
but always return the resumed stream afterward; a gone/never-created job
recovers via the client's existing 404 path instead of being treated as
indefinitely starting. Updated the controller test accordingly.

* 🐛 fix: Gate deduped resume on claim age, not just job presence (Codex review)

Removing the 503 entirely (previous round) reintroduced the inverse race:
if the winning request stalls between claimGeneration and createJob, a
losing duplicate saw no job, returned status:'resumed' anyway, and the
client subscribed to a stream that did not exist yet — the 404 handler
tore the turn down while the winner went on to generate and bill with no
UI attached.

Distinguish the two missing-job cases by claim age (claimedAt now travels
on the claim value):
- fresh claim, no job yet → winner is still starting → 503 SERVER_NOT_READY
  so the client retries via the readiness path (bounded, not indefinite).
- old claim, no job → the original already completed and was cleaned up
  (or the winner died) → attach; the client's 404 handler refetches.

Tests cover both age branches.

* 🐛 fix: Scope dedup fail-open + keep resumed convos on 404 (Codex review)

- Fail-open only on claim acquisition: a store error while checking an
  already-confirmed existing claim no longer falls through to createJob
  (which would start a second billed generation during a Redis hiccup).
  Once claim.existing is known, a job-lookup error returns 503 retry.
- Don't drop a resumed convo on 404: the optimistic-conversation cleanup
  in useResumableSSE now runs only for fresh (non-resume) subscribes. A
  deduped resume whose original completed and was cleaned up 404s, but its
  conversation is persisted and must stay in the sidebar.

Adds a controller test for the job-lookup-error path (503, no createJob).

* 🐛 fix: Reconcile resumed convos on 404 instead of guessing (Codex review)

Round-4's !isResume guard fixed the completed-and-cleaned case (don't drop
a persisted convo) but left the inverse: a new-conversation retry deduped
to a claim whose original worker died before persisting still resumes,
404s, and — with removal skipped — leaves a phantom /c/<streamId> sidebar
entry.

Stop guessing keep-vs-remove on a resume 404. Reconcile against the
server: invalidate the conversations list so a real (persisted) convo
stays and a phantom is dropped. Fresh (non-resume) optimistic streams
still prune immediately. Adds a client test for the resume path.

* 🐛 fix: Finalize failed job before releasing its claim (Codex review)

In the initialization-error catch, the idempotency claim was released
before completeJob(streamId). A racing retry could win the released key
and createJob() the same streamId while this catch was still running, and
completeJob() (not guarded by the original createdAt) would then abort the
replacement. Finalize the failed job first, then release the claim.

Adds a controller test asserting completeJob precedes releaseGeneration.

* 🐛 fix: Clear claims on destroy + survive completeJob failure (Codex review)

- InMemoryJobStore.destroy() now clears the idempotencyClaims map, so a
  reused/reconfigured store instance doesn't dedup a fresh start against a
  torn-down job's stale claim.
- Init-error cleanup: completeJob() is swallowed so a store-hiccup
  rejection can no longer skip the idempotency-key release and the
  pending-request decrement (which would wedge the retry behind the claim
  and leak the concurrency slot). A failed completeJob finalized nothing,
  so releasing afterward still can't abort a later replacement.

Tests: claims cleared on destroy; release + pending decrement still run
when completeJob rejects.
2026-07-21 08:16:31 -04:00
Danny Avila
3171b86413
🎞️ perf: Coalesce Streaming Delta Cache Writes Per Animation Frame (#14332)
* 🎞️ perf: Coalesce Streaming Delta Cache Writes Per Animation Frame

* 🎞️ fix: Cancel Pending Delta Flush Before Standard-Path Terminal Writes

* 🎞️ fix: Flush Queued Deltas at Abort, Error, and Pending-Action Boundaries
2026-07-21 08:02:42 -04:00
Danny Avila
3337bde050
🚏 fix: Route Admin-Configured Document Types to RAG /text on Agent Upload (#14345)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🩹 fix: Route configured document types to RAG /text on agent upload

Restores pre-#11900 behavior for 'Upload as Text': when an admin narrows
fileConfig.text.supportedMimeTypes to a non-permissive allowlist that includes
a document type (docx/xlsx/pdf/ods/odt) and a RAG API is configured, the file
is sent to RAG /text instead of the built-in document parser.

The permissive default catch-all is excluded via isPermissiveMimeConfig, so RAG
deployments that never customized text handling keep the built-in parser. When
RAG is unreachable, parseText's new allowNativeFallback:false makes it throw so
the upload falls back to the built-in document parser rather than degrading a
docx/pdf to raw native-text bytes.

Fixes #14245

* style: sort imports in text.spec.ts (CI import-order)

* 🩹 fix: Scope RAG fallback catch to extraction only, not persistence

The configured-text branch wrapped both parseText and createTextFile in the
fallback try, so a persistence failure after a successful RAG extraction (size
guard, db.createFile, agent-resource mutation) was misread as RAG-unavailable
and retried with the built-in document parser, masking the real error and
risking a duplicate agent-resource mutation. Only the RAG extraction is now in
the fallback catch; a persistence failure surfaces as itself.

Addresses Codex P2 on #14345.
2026-07-20 22:46:28 -04:00
Danny Avila
f4a0e0c194
🧹 perf: Share Voices Store, Gate Timestamp Ticker, Stabilize Greeting Springs (#14335) 2026-07-20 22:43:29 -04:00
Danny Avila
eeb4ea226c
🧭 perf: Warm Conversation Switches with Single-Navigation Focus Intent (#14334)
* 🧭 perf: Warm Conversation Switches with Single-Navigation Focus Intent

* 🧭 fix: Drop Warm Message Cache When Conversation Revalidation Fails

* 🧭 fix: Defer Departing-Convo Refetch and Gate Resume on Revalidation

* 🧭 fix: Gate Stale-Cache Sends During Revalidation and Honor disableFocus
2026-07-20 22:31:44 -04:00
Danny Avila
74de989bde
🚪 fix: Keep Owners From Being Locked Out of Their Own Resource When Sharing (#14347)
* 🔒 fix: Skip revoke for principals also being granted (owner-lockout guard)

bulkUpdateResourcePermissions flushes grants (upserts) before revokes (deletes).
If a principal appears in both updatedPrincipals and revokedPrincipals, the ACL
entry is granted and then immediately deleted, stripping access the caller just
set. This can strip a resource owner's own grant when the share dialog places
the owner in both lists from a client id/idOnTheSource mismatch (OpenID/Entra).

Add a server-side guard: track principals granted in the same request and skip
any revoke for the same principal, so granting wins and owner lockout is
impossible regardless of how the client computes the share diff. Complements the
client-side keying fix in #14317.

Refs #14316

* 🔒 fix: Exclude PUBLIC from grant-wins guard so public-disable is honored

The grant-wins guard must not apply to PrincipalType.PUBLIC. An explicit
public: false disable adds the public principal to the revoke list; a
contradictory payload that also grants public (public in the updated list) would
otherwise skip the revoke and leave the resource public. Disabling public access
must always win. User/group owner-lockout protection is unchanged.

Addresses Codex P2 on #14347.

* 🔒 fix: Move revoke guard inside per-principal try (tolerate malformed entries)

The grant-wins guard read principal.type before the per-principal try/catch, so
a malformed revoke entry (e.g. removed: [null]) would throw out of
bulkUpdateResourcePermissions after grants were already flushed on
non-transactional MongoDB, leaving partial permission changes. Move the guard
inside the try so a malformed entry is recorded in results.errors and skipped,
matching prior behavior.

Addresses Codex P2 on #14347.
2026-07-20 22:27:25 -04:00
Danny Avila
1a58c72444
🧩 chore: Prebundle Node Polyfills for Buffer, Process, and Global in Vite optimizeDeps (#14354)
Enhance the Vite configuration by including specific node polyfills in the `optimizeDeps` section. This change ensures that the necessary polyfills for `buffer`, `process`, and `global` are optimized for better performance during development. This adjustment aims to improve compatibility and streamline the build process.
2026-07-20 21:18:27 -04:00
Danny Avila
d02867a3e2
🔍 refactor: Surface primary OpenID JWT failure reason in auth failure log (#14346)
When OPENID_REUSE_TOKENS is enabled and both the openidJwt strategy and the
HS256 jwt fallback fail, the final 'Authentication failed after all strategies'
warn log reported only the fallback's reason. For an RS256 provider (Keycloak,
Auth0, Okta) that surfaces as 'invalid algorithm', which is the HS256 fallback
rejecting the provider token, not the real reason openidJwt did not
authenticate, and it was previously only visible at debug level.

Include the captured primary (openidJwt) failure reason and error name in the
final warn log so reused-token failures are diagnosable without enabling debug
and are not misattributed to the fallback.

Refs #14311
2026-07-20 21:05:20 -04:00
Danny Avila
d5e8c5c15e
🚰 perf: Suppress No-Op Conversation Writes and Widen-Proof Atom Subscriptions (#14329) 2026-07-20 21:04:34 -04:00
Danny Avila
a8ecdd6226
🧷 perf: Stabilize Mutation-Dependent Memos in Chat Hooks (#14328) 2026-07-20 21:02:14 -04:00
Joshua Blake Smith
33b998064d
🧭 refactor: web_search Description on When To Search vs Answer Directly (#13930)
* fix: stop agents from over-using the web_search tool

The web_search tool context injected into the agent system prompt was framed action-first ("execute immediately without preface") and never told the model when NOT to search. Models treat this as a default-on signal and search on nearly every turn, across all providers (Anthropic API, OpenRouter, etc.) since this instruction is provider-agnostic.

Reframe the instruction so the default is to answer from the model's own knowledge, gate searching behind a concrete trigger list, and explicitly reject the "a search might return relevant results" rationalization. Citation-format guidance is unchanged.

* refactor: make web_search guidance concise and neutral

Address maintainer feedback to keep the change minimal and generalized. Replace the verbose trigger list with a single neutral sentence: search when the user's request calls for it or when current/external information is required, and otherwise answer directly. Demote "execute immediately without preface" so it applies once a search is warranted rather than as the default, which was being read as "search on every turn."
2026-07-20 20:15:42 -04:00
Danny Avila
56ecb6494c
✋ feat: Add Reclaim-Gated Controls to In-Flight Steers (#14321)
* ✋ feat: Add Reclaim-Gated Controls to In-Flight Steers

Give a pending steer the same controls as a queued message — edit, convert
to queue, and the during-run mode toggle — instead of only a bare cancel.

Every re-homing action reclaims the steer from the server queue first and
acts only on a confirmed `removed: true`. A steer leaves that queue only by
injecting, so a lost race means the words are already in the run: queueing
or editing them then would say the same thing twice.

- Return a `SteerCancelOutcome` ('reclaimed' | 'applied' | 'failed') from
  useSteerCancel so callers can distinguish "the words are still mine" from
  "already injected" and "unknown fate" — the last two only toast.
- Extract RowMenu, useDefaultToggleEntry, and the shared button classes into
  SteerMenu so both during-run surfaces use one implementation.
- Offer controls only once `pending`: a `sending` steer has no server id to
  reclaim with, so its words cannot be held back.
- Pin the control cluster visible while its menu is open — the portaled items
  hold focus outside the subtree, so `focus-within` alone would drop it.

* 🩹 fix: Address Codex Findings on Reclaimed-Steer Controls

Route a reclaimed steer through the shared conversion, and stop the async
reclaim from stranding items or clobbering a composer that moved on.

- Queue a reclaimed steer via useSteerConvert instead of enqueue, so it keeps
  its original id and createdAt. enqueue minted a fresh v4/Date.now() and
  appended, so a steer accepted BEFORE a later follow-up drained after it —
  breaking the invariant the leftover-steer path documents.
- Submit the item directly when the run ended during the reclaim round-trip:
  the drain consumes its one-shot signal against an empty queue, so nothing
  was left to auto-send it. Read run state and conversation from refs, since
  the reclaim resolves after the bubble unmounts.
- Refuse the composer restore when the origin conversation no longer matches
  or a newer draft is present, and queue the words instead of overwriting
  them. Neither text is the one to throw away.
- Split useSteerReclaim (POST only) out of useSteerCancel, so the menu actions
  leave the chip alone until the outcome is known while the X stays optimistic.

* 🛡️ fix: Harden Reclaimed-Steer Guards Against Stale State

Both guards from the previous round read values that had moved on by the
time the reclaim resolved.

- Compare the origin conversation against a ref, not the closure. The `.then`
  holds `restoreReclaimedSteer` from the render it was clicked in, so its
  captured `conversationId` is the OLD chat — the guard compared that against
  itself and passed, while `methods` (one form, reused across conversations)
  wrote the steer into the chat now on screen.
- Gate the direct send on the drain's own rule. `!isSubmitting` also covers a
  Stop or an error, so converting and then pressing Stop auto-sent the text
  past useQueueDrain's completed-or-armed-interrupt-only rule. Capture the
  run's outcome before the drain consumes the one-shot signal, and send only
  on a clean completion of THIS conversation.

* ♻️ refactor: Re-Arm the Drain Instead of Direct-Sending Reclaimed Steers

The direct send was the wrong mechanism: it re-implemented the drain badly,
and each round of review found another rule it had skipped. Delete it and let
useQueueDrain do the sending — it already owns every one of those rules.

- Re-post the spent run-end signal under the conversation instead of calling
  sendNow. The drain then applies the completed-only rule, FIFO order (an
  older follow-up is no longer skipped), NEW_CONVO migration, and submits via
  `ask` — which, unlike the composer's sendNow, does not reset the form and so
  cannot wipe a draft typed while the reclaim was in flight.
- No-op when a signal is still armed: that drain has not run yet and will see
  the item on its own, so arming a second carrier would send twice.
- Watch the parked run-end too, not just the index one, so a run that ended
  while the user was in another chat is still seen.
- Treat staged files, quotes, and skill picks as a draft when deciding whether
  a restore may overwrite the composer — editToComposer MERGES into them, so
  restoring over staged context would glue two submissions together.

* 🎯 fix: Scope the Re-Arm Suppression to This Conversation

The no-double-arm guard treated ANY armed index run-end as proof the drain
would see this conversation's newly queued item. The index slot is shared:
useQueueDrain parks a foreign signal under its own conversation and then
inspects only the active one's queue, so a reclaimed steer sitting behind an
unrelated run-end would never be looked at and would strand until sent by hand.

Suppress only when the armed index signal belongs to THIS conversation — which
is the case where the drain really will see the item. The parked check was
already conversation-scoped by its key.

* 🧭 fix: Trust the Refs Only While They Describe This Chat

useSteering is reused across conversations, so after a navigation its live
refs describe the NEW chat while the reclaim's callback still speaks for the
old one. Restoring the conversation-identity guard I removed last round, which
was wrong precisely because the refs are live but not conversation-scoped.

- Skip the re-arm entirely once conversationIdRef no longer matches the steer's
  conversation. Reading isSubmittingRef there could suppress a needed re-arm,
  and lastRunEndRef could hold the NEW chat's run-end — parking that under the
  old conversation would make drainNext (which keys off end.conversationId)
  drain the wrong queue into the wrong chat.
- Assert lastRunEnd.conversationId matches before re-arming, so the invariant
  is enforced where it is relied on rather than inferred from render order.

Nothing is lost by stopping: the item is already queued under its own
conversation, and that run's end parks under it and drains on return.

* 🗝️ fix: Key the Captured Run-End by Conversation

A single run-end slot could only answer for whichever chat was on screen when
a reclaim landed, so the guard had to bail on navigation — stranding a steer
whose run had already completed, contrary to "queue for after the response".

Key the captured run-ends by conversation instead. The stored end always speaks
for the chat the words belong to, so navigating away no longer suppresses the
re-arm, and another chat's end can never be parked under this one (which would
hand drainNext a foreign end.conversationId and drain the wrong queue).

- Drop an entry when its conversation starts another run: a superseded end must
  not authorize a drain of the run now in flight. This replaces the isSubmitting
  guard, which described the wrong chat after navigation.
- Remove conversationIdRef, now that no read depends on where the user is.

* 🧹 fix: Close Three Reclaim Races Around Answer Mode and Run End

- Refuse the composer restore while answer mode is active. `onSubmit` hands
  composer text to `answerMode.submitText` before any send/steer routing, so a
  restored steer would become the tool's answer on the next Enter. Read through
  a ref: the run can pause on ask_user_question mid-reclaim.
- Skip the restore when a terminal conversion already queued the words. The
  chip stays interactive during the reclaim round-trip, so a run ending or
  erroring meanwhile converts it — restoring after would leave one copy queued
  and another in the draft. The queue action needed no guard; the conversion
  already dedupes by id.
- Carry quotes/skill picks on the reclaimed steer itself. The conversion
  recovers them from the chip, which a competing X can delete mid-round-trip,
  silently dropping the picks.

* 🎛️ feat: Fold Cancel Into an Always-Visible Steer Menu

Make the in-flight steer bubble a single, discoverable affordance instead of
two hover-hidden ones, matching how Codex/ChatGPT present the same control.

- Fold Cancel into the ⋯ menu as an item (X icon), removing the standalone X
  button. It keeps the optimistic `useSteerCancel` path — no reclaim gate,
  since cancel drops the words rather than re-homing them.
- Show the ⋯ at rest on every pointer instead of hover-gating it. A label-less
  menu hidden until hover is undiscoverable on desktop and unreachable on touch;
  always-visible also matches the queued rows' controls and drops the
  hover/focus/menu-open opacity juggling entirely.

* 🪢 feat: Make Cancel and Queued Trash Non-Destructive

Both removal actions now hand their text back to the composer instead of
dropping it, so a message the user typed is never gone forever.

- In-flight Cancel: before cancelling, restore the words to the composer via
  the gated `restoreReclaimedSteer` (skipped once applied — they are already in
  the response). The restore refuses on its own rather than clobber a draft,
  land in another chat, or fight answer mode; the cancel still runs reliably
  either way, so an unwanted steer stays killable.
- Queued Trash: same safety net — thread the gated restore into the queued
  rows and return the words (with their carried quotes/skills) to the composer,
  then remove either way. Aligns the two surfaces on one behavior.
- Export the shared `RestoreToComposer` type so both surfaces reuse it.

Left the reliable-remove path intact (a steer sometimes must be killed before
it reaches the model) and did NOT reach for a delete+Undo snackbar, which the
shared Toast can't render without a cross-cutting action-button change.

* 🔒 fix: Restore Cancelled Steer Text Only on a Reclaimed Outcome

The cancel safety net restored the words to the composer synchronously, before
the cancel POST resolved. On `applied` (cancel lost the race, steer still
injects) or `failed` (POST errored, chip restored), the same text ended up both
in the run/bubble and in the composer.

Await `cancelSteer`'s outcome and restore ONLY on `reclaimed` — the one result
that proves the steer never reached the run. `applied`/`failed` leave the words
where the events place them, no composer copy. The gated restore still refuses
rather than clobber a draft typed during the round-trip.

* 🧵 fix: Never Drop Cancelled Text; Keep the Steer's Submit Time

Two follow-ups on the cancel safety net.

- Cancel no longer silently drops the words when the reclaim succeeds but the
  composer refuses the restore (draft typed, answer mode, navigated). The chip
  is already gone, so queue them like Edit does — never lost, just re-homed —
  with the same toast.
- Preserve the true submission timestamp across submitSteer's chip states. The
  ACK and failure chips reset createdAt to a LATER Date.now(), so a draft queued
  during the 202 round-trip could sort ahead of a steer submitted before it and
  drain out of order. Capture the submit time once and reuse it for all three.

* 🚪 fix: Refuse Reclaimed-Steer Restore Into an Unmounted Composer

A reclaim/cancel round-trip can resolve after ChatForm unmounts (left the
route, closed the pane). Its refs still hold the origin conversation, so
`restoreReclaimedSteer` passed its checks, wrote into a dead form, and returned
true — making the caller drop the steer instead of queueing it, losing the text.

Track mount state and refuse the restore once unmounted, so the caller queues
the words (recoil is global, so the queued chip survives the navigation).
2026-07-20 20:08:38 -04:00
Anmol
5cf849d8d5
🪆 fix: Build RAG-API Chart Before Parent to Bundle PostgreSQL Dependency (#14262)
Co-authored-by: anmol-kumar-us <anmolsrivastav.lw@gmail.com>
2026-07-20 20:01:24 -04:00
Danny Avila
99f2cc8c7d
🧵 fix: Harden Request-Scoped MCP Lifecycle (#14342)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix: harden request-scoped MCP lifecycle

* chore: sort MCP integration test imports

* fix: preserve MCP activity across connection replacement
2026-07-20 13:42:19 -04:00
Danny Avila
8e5ef1fb31
⚡ perf: Halve Per-Delta Redis Round Trips in Resumable Streams (#14313)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
`RedisEventTransport.emitChunk` awaited `INCR` (sequence allocation) and then
`PUBLISH` as two sequential round trips, per streamed delta. Fold both into one
Lua `EVAL` that allocates the sequence and publishes server-side.

Measured 50.6% reduction on the emit path (0.219ms -> 0.108ms per delta, 2000
deltas, loopback Redis). The saving multiplies by the token count of every
response, and scales with RTT: on setups where Redis sits behind a network
boundary (WSL2 loopback, cross-host, cross-AZ) at ~1-2ms/RTT this is ~0.5-1s on
a 500-token response.

The sequence is spliced into the payload server-side rather than round-tripped
through cjson, which would coerce empty arrays to objects and alter float
precision. The channel is passed as ARGV rather than KEYS: ioredis applies
`keyPrefix` to EVAL keys but never to a pub/sub channel, so keying it would
publish to a prefixed channel no subscriber listens on. PUBLISH is broadcast
cluster-wide rather than slot-routed, so it needs no key for Cluster
correctness.

Also parallelize `getResumeState`'s three independent job-store reads
(`getContentParts` / `getRunSteps` / `peekSteers`), collapsing 3 round trips
into 1 on every resume. Safe despite `readCachedGraph`'s cache-drop side effect:
each call catches its own unusable-graph throw and falls back to durable
reconstruction, so ordering cannot change the result.

`createMockPublisher` gains an `eval` that delegates to its own incr/publish
mocks, keeping the existing error-propagation tests meaningful now that
sequence allocation and publish are one operation.
2026-07-16 11:30:42 -04:00
Danny Avila
b04ff2648e
📱 fix: Don't Connect the Favorites Drag Source on Touch Pointers (#14312)
#14272 gated the hover-revealed "..." button on hover capability, but pinned
agents still take two taps on iOS. That fix was aimed at the wrong mechanism
for this list.

Every favorite row is wrapped by DraggableFavoriteItem, and react-dnd's
HTML5Backend stamps `draggable="true"` on that wrapper unconditionally
(connectDragSource, HTML5BackendImpl.js:101 — `canDrag: false` does not
suppress it, react-dnd#2909). iOS Safari hands a touch on a draggable element
to the drag recognizer rather than synthesizing a click, so the row underneath
only selects on the second tap.

The draggable wrapper is what separates favorites from every other sidebar
row. Conversation rows are more hover-dependent than favorites ever were
(ungated `opacity-0 group-hover:opacity-100` plus an onMouseEnter that mounts
ConvoOptions) and select on the first tap.

Connect the drag source only under `(hover: hover)`. Nothing is lost on touch:
HTML5Backend has no touch support, so drag-to-reorder never worked there.
Passing null to the connector unsubscribes cleanly and resets the attribute,
so a hybrid pointer flipping the query re-arms drag.
2026-07-16 11:29:07 -04:00
Danny Avila
bd1df30b7d
🔒 fix: Scope, Cap, and De-Execute the In-Flight Steer Stack (#14310)
* 🔒 fix: Scope, Cap, and De-Execute the In-Flight Steer Stack

Codex review on 9594ee7146. Three valid P2s, all fallout from moving the
steers out of the message region into the composer.

- Run scope: the in-thread slot was gated on `effectiveIsSubmitting`, but
  the new one only checked `steering.enabled` (= steerable endpoint +
  primary composer), which is true with no run in flight. A chip that
  outlives its run — cancel's onError restoring one the final event
  already converted to a queued follow-up — stranded a bubble above the
  composer, possibly beside the queued row for the same text. Restores
  the run gate.
- Height cap: a steer runs to 16k chars (DEFAULT_STEER_MAX_LENGTH) and a
  run takes up to 10 (STEER_QUEUE_MAX_DEPTH). Unbounded in the composer,
  that pushes the input off-screen; the old slot could grow freely
  because it scrolled with the thread. Caps the stack at 35vh.
- Code execution: MarkdownLite defaults `codeExecution` on, but this
  bubble renders outside MessageContext, so Run Code would fire the tool
  mutation with no messageId and an empty conversationId. Passes
  codeExecution={false} — a provisional steer has nothing to run against.

* 📜 fix: Keep the Newest In-Flight Steer in View

Codex review on de9ede2aad. Valid, and a regression from the 35vh cap in
the previous commit: steers append newest-last, so once the stack
overflows it sits scrolled to the OLDEST entry. The steer just submitted
— and its cancel control — lands below the fold and reads as dropped.

The cap traded "composer pushed off-screen" for "newest steer hidden".
Sticks the stack to the bottom, keyed on the newest steer id so it fires
when one is appended rather than on every render.

* 🧹 fix: Don't Restore a Steer That Already Settled

Codex review on 09c93987a. Valid, and it closes the hole the run gate
only hid — I deferred this two rounds ago as pre-existing, which was
wrong: the gate hides a stale entry while the run is idle, but
useQueueDrain auto-sends the queued follow-up, isSubmitting flips back
to true, and the previous run's entry renders as an in-flight bubble
beside its own queued copy.

Fixes it at the source instead: cancel's onError no longer restores a
steer whose id is in appliedSteerIdsByConvoId — the settled set, stamped
by both the apply path and the run-end conversion, and deliberately
capped rather than cleared so it survives run end for exactly this race
(same instrument as #14276).

The run gate stays: it's parity with the in-thread slot's
effectiveIsSubmitting and still defends against any other leak.
2026-07-16 11:14:29 -04:00