* ⚡ perf: Swap the Transcript With the URL on Conversation Switch
Switching conversations left the PREVIOUS transcript painted under the new
URL. Two things on the critical path caused it, both fixed here.
`RouterProvider` commits location updates inside `React.startTransition` by
default in react-router v7, and a transition keeps the outgoing tree on
screen until the incoming one has fully rendered — so every millisecond the
next thread took to render was time spent looking at the previous one, and
React yields during that render, stretching it well past its CPU cost.
Nothing here reads route data through router loaders, so the transition
bought no pending UI; conversation state also still lives in Recoil, whose
transition-safe reads are gated behind `_TRANSITION_SUPPORT_UNSTABLE` hooks
this app does not use. `useTransitions={false}` puts the route change back
in the click's own task.
`navigateToConvo` also awaited `GET /api/convos/:id` before calling
`navigate()`, so the route did not change until a full server round trip
completed. The clicked row already carries its conversation, so the route
and conversation state now change together and the refetch reconciles
afterwards. The row is a list projection, so any previously fetched full
record underlays it — prompt prefix, sampling params and files survive the
switch, and a send during the reconcile window still carries the real
settings.
Measured on the built client with a 250ms conversation-fetch latency,
switching between two 30-turn conversations:
before cold click→url 527ms click→paint 931ms 14 stale frames (297ms)
warm click→url 474ms click→paint 838ms 12 stale frames (277ms)
after cold click→url ~190ms click→paint ~450ms 0 stale frames
warm click→url ~280ms click→paint ~280ms 0 stale frames
The warm switch now paints the new transcript in the same commit as the URL.
The warm-cache message loading this depends on is untouched.
Adds `e2e/benchmarks-navigation`, a react-scan benchmark that guards the
result: an in-page sampler records the route and the mounted conversation
once per animation frame, so a frame pairing the next URL with the previous
transcript is caught directly. The react-scan harness the reasoning
benchmark had inlined moves to `e2e/perf/scan.ts` and is now shared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qZJDNkyH5rgCz6KcLjseq
* 🎯 fix: Resolve Sidebar Rows by Their Accessible Button in the Nav Benchmark
The a11y pass on the sidebar moved the conversation row's `role="button"`
and `aria-label` off the `convo-item` container and onto a real `<button>`
that `ConvoLink` renders inside it. The benchmark's click helper required a
single node carrying both the testid and the label, so after merging dev it
found nothing and threw.
Match on whichever node inside a row carries the label and let the click
bubble to the container's handler, which still owns the navigation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qZJDNkyH5rgCz6KcLjseq
* 🛡️ fix: Close Three Navigation Races Found in Review
Codex review of the optimistic-navigation path found three real defects.
Superseded reconciliations were written unconditionally. Selecting B then C
before both records settled let B's response land last and restore B into
conversation state while the route and transcript showed C — and sends read
from that state, so a user could submit into a conversation they were no
longer looking at. Navigations now claim a shared token before any await and
late responses are discarded. The token is module state rather than a ref
because every sidebar row mounts its own hook instance, so a ref cannot see
that a click on a different row superseded this one.
The first visit to a conversation installed the sidebar row as active state.
That row is a projection without prompt prefix, sampling params, tools or
files, so the composer became usable with settings that silently fell back to
defaults. Only a conversation whose full record is already cached now takes
the instant path; the first visit keeps the previous behavior and moves the
route once the record is in hand. Every later switch to it is instant, which
is the case this PR set out to fix.
A failed record fetch removed the target's message cache even though, after
optimistic navigation, that query is already mounted — a transient error
could cancel an in-flight history fetch, or discard one that had succeeded,
with no route change left to remount it. That removal is now limited to a
conversation confirmed gone, and the first-visit path still clears before the
route moves, where a fresh mount follows.
The benchmark's round-trip assertion was also unfalsifiable: nothing delayed
the record request, so an implementation that awaits it still answered inside
the threshold. It now holds that request open and asserts the warm switch
completes while it is unresolved, which no wall-clock bound can fake.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qZJDNkyH5rgCz6KcLjseq
* 🧭 fix: Tie Pending Navigation Work to the Route, Not a Token
Codex found that the navigation token only tracked calls made through this
hook. Every other way out of a conversation — `useNewConvo`, a link, a
redirect, the back button — moves the route without touching it, so a record
still in flight for the conversation being left passed the guard. On the
cached path that overwrote the new route's conversation state; on the
first-visit path it was worse, calling `navigate()` and pulling the user back
into a chat they had already left.
The token was the wrong question. What makes pending work still wanted is not
"was this the last conversation clicked" but "is the user still where they
were when it started" — and only the browser's own location sees every way
that can change. Each async step now captures the route before its request
and re-reads it before writing, which subsumes the superseded-click case the
token was added for and removes the module state entirely.
Reading `window.location` directly rather than `useLocation` keeps this free
of subscriptions: every sidebar row mounts this hook, so subscribing would
re-render all of them on every navigation — the cost this hook exists to
avoid. Comparing pathname against pathname also makes the basename cancel.
The tests move from `MemoryRouter` to a real history, since the mechanism is
now the browser location itself, and cover both bypass paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qZJDNkyH5rgCz6KcLjseq
* 🔢 fix: Keep the Last Click Authoritative Across First-Visit Navigations
Codex found that the route guard cannot separate two first-visit clicks from
each other. That path deliberately leaves the route where it is until the
record arrives, so clicking two uncached conversations in quick succession
has both requests capture the same pathname — whichever the network answered
first then navigated, and the later click was discarded. Response order
decided where the user landed instead of click order.
Restores a generation counter alongside the route check. Claiming the last
PR's removal of the token as a subsumption was wrong: the two guards answer
different questions and neither covers the other. The generation says "a
newer intent replaced this one"; the route says "the user left by some means
this hook never saw". Both are needed, and both are cheap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qZJDNkyH5rgCz6KcLjseq
* 🧹 refactor: Stop Writing Server Snapshots Into User-Owned Conversation State
Four review findings on this branch were all the same defect: navigation
started a background fetch and wrote its result into the conversation atom.
That atom is user-editable — model, endpoint, prompt prefix, sampling params
— and the target chat is interactive from the moment the route changes, so a
late write races the user and every other writer. Each round added another
predicate to the write ("is this still the last click?", "is the user still on
this route?"), and each predicate left one more writer uncovered; the last one
is a setting picked on the same route by the same navigation, which no
ordering or route guard can see.
Remove the write instead of guarding it. The warm path refreshes the React
Query cache and stops there, so the optimistic merge that lands with the route
is the navigation's last word. The refreshed record is consumed by the next
switch to that conversation, which is where a cached record is read anyway.
This also dissolves the queued-focus finding: `applyConversation` (and its
`requestChatFocus`) is now reachable only from paths that navigate, so a focus
intent can no longer outlive the navigation that requested it.
Scope the synchronous route commit to conversation switches. `useTransitions`
on the provider disabled transitions for every route, including the lazily
loaded prompts, skills, insights and project screens, where yielding to input
during a large first render is worth more than an atomic swap. The opt-out
now travels per navigation as `chatNavigation` (`flushSync`), applied in
`useNavigateToConvo` and `useNewConvo`.
Tests: the four behavioural guards fail against an implementation that
restores the background write, including a new case where the user picks a
model while the refresh is in flight.
* 🎯 fix: Decide Route Commit Once, and Keep Refreshed Settings Refreshed
Reverts the per-navigation transition opt-out from the previous commit. Review
asked to scope `useTransitions={false}` to conversation switches, and I scoped
it by passing an option at the call sites I knew about — then immediately
missed one: `finalHandler` promotes `/c/new` to the server-assigned ID and
navigates without it, so the atom identifies the real conversation while the
route and message query still say `new`.
That is not a missed call site, it is the wrong shape. Fourteen call sites
across components, chat hooks and SSE handlers navigate into `/c/*`; an opt-out
carried by each one is a list that rots as call sites are added, and five of the
fourteen were covered. The property is route-shaped, so the decision goes back
to the one place that sees every navigation. Answering the original critique on
its merits: nothing in the app reads route data through router loaders or
renders pending UI from `useNavigation`, so the transition produces no
interstitial on any route — it only defers the commit, which on the chat route
is the bug this PR exists to fix.
Two conversation fixes alongside it:
Sidebar rows no longer reinstate settings the background refresh replaced. The
row projection carries `endpoint`, `model` and `spec`, and the warm path
spreads the row over the cached record — so a row from before an edit made on
another device would undo that edit on every switch until the list refetched.
The refresh now merges the record into the list cache, which is what made
"picked up on the next switch" true rather than merely intended.
Starting a new chat now supersedes a pending first visit. "New chat" from
`/c/new` lands on `/c/new`, so the pathname is unchanged and the record for a
conversation the user just abandoned would land and pull them into it. The
navigation counter is exported as `supersedeNavigation` and called from
`useNewConvo`. Deliberately not called from the stream recoveries in
`useEventHandlers`/`useChatFunctions`: those are the app reacting, not the user
changing their mind, and they should not cancel a conversation the user opened.
Intent is a closed set; navigation is not.
Both new tests fail against the implementation they guard.
* 🧷 fix: Keep the Record Refresh Off List State and Off Background Composers
Three fixes to the previous two commits, all the same underlying mistake in
different places: something that started earlier landing on top of something
the user did later.
The list-cache write added last commit merged the whole fetched record into
every sidebar and pinned row. That response is a snapshot from before the
target was interactive, and the list is where renaming, pinning and sharing
land — so a rename completing while the request was in flight was silently
undone. This is the same stale-snapshot-over-live-state mistake the refresh
had just stopped making against the conversation atom, reintroduced one layer
down. It now writes `endpoint`, `model` and `spec` only, which is what the
staleness it exists to fix is about, and which no list mutation touches.
The route comparison ignored the query string. `/c/new?projectId=A` is a
different conversation scope than `/c/new`, and the landing chip re-scopes a
draft by writing the atom and rewriting search params in place — never through
a conversation hook, so neither the pathname nor the recorded intent moved. A
pending first-visit record would then land on the draft the user had just
re-scoped. The comparison now includes `search`.
Superseding moved from `switchToConversation` into `newConversation`, guarded
by `keepComposerState`. That flag marks a call that re-renders a composer an
earlier call already opened — agent metadata arriving late, for instance. The
user asked for nothing there, so it must not cancel a conversation they clicked
while it was in flight. `switchToConversation` has no callers outside this
hook, so the move loses no coverage.
The first two are covered by tests that fail against the implementation they
guard. The third is verified by inspection: exercising it needs the whole
`useNewConvo` provider tree, which is disproportionate for a one-line guard.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Every Playwright job spent a flat 90s on `npx playwright install ffmpeg`,
and none of them ended up with a usable ffmpeg.
The 2.3MB download finishes in under a second; extraction then hangs until
`timeout -k 10 90` reaps it (exit 124, masked by `continue-on-error`). That
is a Node 24.16.0 readable-stream change (nodejs/node#62557) colliding with
yauzl/fd-slicer never firing `close` after EOF, which hangs extract-zip.
It leaves a truncated `ffmpeg-linux` — 5,055,201 bytes against the zip's
declared 5,101,056, segfaulting on exec — and no INSTALLATION_COMPLETE
marker, so Playwright treated ffmpeg as uninstalled. `video: 'on-first-retry'`
has therefore never worked in CI, and every first retry of a flaky test died
in browserContext.newPage: exactly the failure the step existed to prevent.
Upstream fixed it in Playwright 1.60.0 (microsoft/playwright#40747) and Node
reverted it in 24.18.0 (nodejs/node#63834). Node 24.16.0 is pinned in 17
places including the Dockerfiles, so bump Playwright instead — it is a dev
dependency, and `^1.56.1` already permitted 1.62.1; only the lockfile pinned
it. Staying at or above 1.62.1 also avoids the tsconfig-resolution
regressions in 1.62.0.
Caching alone could not have fixed this: a cold cache still hangs, and what
would have been cached is the corrupt binary. So the ffmpeg download is now
restored from cache keyed on the resolved playwright-core version, the
install is skipped outright on a hit, and the cache is only saved once the
binary is verified to actually execute — a partial extraction can never be
promoted into a cache that every later job restores.
Per job: 90s to ~0s on a hit, ~2s on a miss.
* 📉 perf: Bound Early Event Buffering for Detached Generations
A generation streaming with no attached subscriber re-entered buffering
mode on every disconnect and retained each emitted event in
earlyEventBuffer for its remaining duration. A single 26-minute detached
run (~58,800 tool-argument deltas) grew the heap past 2 GiB with GC cost
climbing alongside it, while client reconnects always resume from
durable state and discard that local buffer anyway.
- Close the early buffer after the first attachment drains it in Redis
mode; the durable chunk log and pub/sub own recovery from then on,
matching how cross-replica subscribers already attach.
- Enforce hard bounds (5,000 events / 8 MB estimated) in both modes; on
overflow the buffer is discarded and closed, with recovery falling back
to the durable chunk log (Redis) or resume snapshot (in-memory).
- Add a generation_stream_early_buffer_overflows_total counter and
earlyBufferedEvents/Bytes gauges on getRuntimeStats() for visibility.
- Add incident-shaped regression tests and update specs that pinned the
old post-disconnect re-buffering contract.
* fix: redirect post-overflow first attachments to resume recovery
A buffer discarded by the overflow guard left the initial non-resume
SSE attachment with nothing to replay, silently omitting pre-attach
output until the final event. Track the overflow on the runtime and
close such attachments with the existing reconnect signal instead: the
client already re-attaches with resume=true on transport failure and
its sync frame reconstructs the discarded output from durable/snapshot
state. Adds no per-event work; the check is one boolean per attachment.
* fix: enforce buffer bounds when restoring canceled resume captures
Captured emissions restored by a resume canceled before activation
bypassed the early-buffer hard cap, so one oversized restoration could
persist past the limits with no later emission to trip the guard.
Restoration now applies the same overflow-and-close behavior through a
shared helper, and the restore-cap spec fails before this change
(5 events / ~10MB retained) and passes after.
* chore: add Redis management scripts and update package.json for Redis commands
Atomic file claiming (#11675) added a unique partial index on
(filename, conversationId, context, tenantId) for execute_code outputs.
Records written before it inserted a new document per regeneration, so
any deployment that re-ran a cell producing the same filename carries
duplicates the index cannot span: Mongo aborts the build with E11000 and
the constraint is silently absent — the claim path still works, but
without its database-level guard against concurrent inserts.
Adds config/migrate-code-file-duplicates.js to normalize that legacy
data, following the existing migration conventions (dry-run default,
--batch-size, runAsSystem for cross-tenant scans).
Renames rather than deletes: each duplicate is a distinct stored object,
typically still referenced by a message attachment, so removing one
would strip a real artifact from a user's history. The newest record
keeps the canonical name — matching the claim path's latest-write-wins
behavior — and older copies gain a ' (n)' suffix that skips names
already taken in the conversation. Attachments embed their own filename,
so rendered history is unchanged.
After a successful apply the script builds the index directly (targeted
createIndex, not syncIndexes) so the operator learns immediately whether
the constraint is now in place.
Removes 26 of the 32 Rollup-era devDependency declarations left behind when
these packages moved to tsdown, plus two stale config references and an
override that went inert in #14483.
- Drop all 8 from `packages/api`, all 8 from `packages/client` (including
`concat-with-sourcemaps`), and all 10 from `packages/data-schemas`. None of
their tsdown configs import anything from rollup, and none has a rollup
script or config file.
- Keep all 6 in `packages/data-provider`. Five of them back the `rollup:api`
script, which the "Circular dependency checks" CI job runs to surface
rollup's circular-dependency warnings, and `@rollup/plugin-replace` is
imported directly by that package's tsdown config.
- Drop `rollup.config.js` (exists nowhere in the repo) and
`server-rollup.config.js` (real, but never read by the `build` task, so
listing it only caused spurious cache invalidation) from `turbo.json`.
- Drop the `**/rollup.config.js` glob from `eslint.config.mjs`. It matches
nothing, and never matched `server-rollup.config.js`.
- Drop the root `svgo` override, dead since #14483 removed
`rollup-plugin-postcss`, the only thing that pulled svgo into the tree.
Removes three of the eight deprecation warnings emitted on `npm install`.
- Drop `@types/winston` from `packages/api` and `packages/data-provider`.
The published tarball ships no type declarations at all, so `winston`'s own
types were already being used. Declare `winston` as a devDependency instead,
since both packages `import type { Logger } from 'winston'` and were relying
on hoisting to resolve it.
- Drop `rollup-plugin-postcss` from `packages/client`. It is unreferenced since
the package moved to tsdown, and pulled in `cssnano -> postcss-svgo -> svgo@2`,
which is the only consumer of the deprecated `stable`.
- Override `test-exclude` to ^8 so `babel-plugin-istanbul` stops resolving
`test-exclude@6`, which pins the deprecated `glob@7`.
The remaining five warnings (`ldapjs`, `whatwg-encoding`, `node-domexception`,
and workbox-build's `glob`/`source-map`) are transitive with no non-deprecated
version available upstream.
Closes GHSA-r28c-9q8g-f849 (CVSS 7.5, CWE-22), a path traversal in
previous source map auto-loading via sourceMappingURL that allows
arbitrary .map file disclosure. Affected range is <=8.5.17, so the
prior 8.5.13 pin was flagged high by npm audit.
Raises both the root overrides entry, which governs the single copy
in the tree, and the client devDependency floor.
* ⚡ 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.
* feat: ask_user_question tool — agent-initiated questions with durable pause/resume
The HITL runtime merged in #13942/#14024/#14025/#14123 already ships the full
ask_user_question lifecycle (payload-agnostic handleRunInterrupt, resume
validation via mapAskUserAnswer, reconnect rehydration, and the client question
card) — but nothing ever raised the interrupt. This adds the producer:
- packages/api/agents/hitl/askUserQuestionTool.ts: LLM-callable tool whose func
calls the SDK askUserQuestion() helper (LangGraph interrupt() from the tool
body); zod schema with length caps mirroring AskUserQuestionRequest, plus a
JSON-schema twin for the schema-only registry
- Registration: agentToolDefinitions, manifest.json (Tools dialog, admin
filteredTools/includedTools kill switch), basicToolInstances, handleTools
constructor branch
- run.ts gating: checkpointer now attaches for hitlCapable runs whose agents
carry the ask tool even with the tool-approval policy disabled (the interrupt
needs only durability, not humanInTheLoop/hooks); the tool is stripped
fail-closed from non-HITL callers (OpenAI-compat/Responses) and subagent
child configs; excluded from eager event execution (interrupts must be
raised inside the Pregel task frame)
- resume.js: 16k length cap on the answer wire field
- e2e (real Run + FakeChatModel + LazyMongoSaver + supertest resume): tool-body
interrupt pauses durably with NO approval policy, answer round-trips as the
ToolMessage content, tool body re-runs once on resume, sequential questions
re-pause
* fix: adversarial-review findings — in-graph execution, orphan prunes, endpoint scoping, real kill switch
Pre-PR multi-agent review confirmed 5 defects in the initial commit; all fixed:
1. CRITICAL — the tool never paused on the real agents endpoint: production
loads tools definitions-only, flipping the SDK ToolNode to event-driven
dispatch, and the host ON_TOOL_EXECUTE handler runs outside the Pregel task
frame (under runOutsideTracing), where interrupt() throws and becomes an
error ToolMessage. Reworked: the ask tool never rides toolDefinitions/
toolRegistry — on HITL-capable top-level agents a real instance is supplied
via AgentInputs.graphTools (agents#289, requires @librechat/agents > 3.2.57),
the SDK's in-graph direct-tool seam; new production-shape e2e pins the
event-driven mode end to end.
2. CRITICAL — ask-only runs left orphaned interrupted checkpoints (silent
context duplication on every later turn): both orphan prunes were gated on
toolApproval.enabled. The pre-turn prune now also fires for ask-capable
agents (exported agentRequestsAskUserQuestion), and the abort-route prune
fires when the aborted job carries a pendingAction.
3. MAJOR — self-spawned subagents bypassed the strip (self config resolves from
the parent's _sourceInputs): fixed SDK-side (buildChildInputs clears
graphTools) and the tool is now never present on child surfaces host-side.
4. MINOR — the manifest entry leaked into the Assistants tools dialog and the
legacy plugins endpoint, where tools execute with no run to pause: new
agentsOnly manifest flag, scoped out of both listings.
5. MINOR — filteredTools/includedTools only hid the tool from the dialog:
now enforced at run build (strip + no checkpointer), making the admin
filter a real kill switch for already-saved agents.
* chore: update @librechat/agents dependency to version 3.2.58 in package-lock.json and package.json files
* fix: reject agents-only tools at assistant create/update (Codex round 1)
The tools-dialog scoping keeps ask_user_question out of the assistants
LISTING, but the v1/v2 create/update handlers resolve arbitrary posted tool
strings from the shared getCachedTools map — a REST client or stale saved
payload could still attach it, and the assistants runtime executes tools with
no run to pause, so every call would error. New isAgentsOnlyTool(tool)
(manifest-driven, handles string and function-object shapes) drops such tools
with a warn at all four resolution sites (v1+v2, create+update).
* fix: offset resumed-run content indices past the pre-pause seed
A resumed run rebuilds the graph from the checkpoint, and the fresh graph
numbers content indices from its own empty contentData — starting at 0. The
resume path seeds the (also fresh) content aggregator with the pre-pause
parts at exactly those indices, so the resumed model turn collided with the
seed: type-matching parts silently MERGED (post-resume text appended into a
pre-pause text block), and type-mismatching parts (a reasoning/think part at
index 0 — any Anthropic reasoning agent) dropped EVERY delta with 'Content
type mismatch', losing the entire post-resume output from the live stream
and the saved message.
Latent since #13942 — tool-approval resumes corrupt content the same way
(probe-verified); it surfaced now because ask_user_question makes pausing a
first-class flow and reasoning models make the loss total.
- createContentIndexOffsetHandlers(handlers, offset): wraps ON_RUN_STEP
(the single point where a content index enters the pipeline — deltas
resolve through the aggregator's stepMap) and ON_AGENT_UPDATE's inline
index; every other handler passes through by reference. Probe-validated:
resumed output now lands as a new part after the paused tool call.
- resumeCompletion wires it with offset = seedContent.length.
- logToolError: a GraphInterrupt unwinding out of a tool body is the HITL
pause working as designed — no longer logged as a Tool Error.
* fix: unblock live streaming of the resumed segment after an answer
With resume indices now ABSOLUTE (server continues after the pre-pause
parts), the synthetic ask-user-question card was squatting on exactly the
index the resumed segment streams into: applyAskUserQuestion appends the
card at the end of the message content, so on the answering device every
incoming part at that index was blocked and nothing rendered between the
answer submission and the finalize replacing the message.
removeAskUserQuestionPart(message, actionId) strips the pause-scoped card
on successful answer submission (useResumeSubmit onSuccess) — the durable
record of the Q&A is the ask_user_question tool call itself. Pure helper +
specs; same-reference no-op when nothing matches.
* fix: displace the synthetic question card in the streaming content writer
The store-level strip on answer submit wasn't enough: the SSE step handler
keeps its own in-flight copy of the streaming message, so on the answering
device the synthetic ask-user-question card still occupied the ABSOLUTE index
the resumed segment streams into — every delta warned 'Content type mismatch'
(existing ask_user_question vs incoming text) and nothing rendered between the
pending_action and finalize.
Displace the card inside updateContent when any real part claims its slot —
the same displacement pattern as the OAuth prompt part directly above it.
Covers the streaming handler's own copy, reconnecting tabs, and other devices;
once real content streams, the pause is over by definition. Spec drives a
runStep + text delta into the card's index and pins: no mismatch warn, card
gone, text rendered.
* feat: dedicated UI + durable data for completed ask_user_question calls
The completed ask call rendered as a generic tool card labeled 'Cancelled'
with raw (and empty) JSON args. Two layers fixed:
Data: the saved tool_call part had args:'' and no output — streamed arg
chunks carry no tool name so the aggregator drops them (normal tools recover
via the completion event, which never fires for a tool that interrupts
mid-execution and resumes on a rebuilt run with no step id). The resume
controller now stamps the paused ask part with the pendingAction's
authoritative question as args and the user's answer as output
(attachAskUserQuestionAnswer — pure, targets the newest unanswered ask part,
so sequential questions each keep their own answer).
UI: Part.tsx routes ask_user_question tool calls to AskUserQuestionCall — a
compact Q&A record ('Asked a question' header, question, description, 'You
answered: <label>' preferring the picked option's label, or 'No answer was
given' for an abandoned pause) instead of the generic card. New i18n keys;
parseAskUserQuestionArgs degrades to null on malformed model args.
* fix: single question UI per pause + immediate answer display
Two live-turn issues with the new durable Q&A card:
1. Duplicate question on ask: during a live pause the message carries BOTH the
ask tool_call part (now rendered by AskUserQuestionCall, showing a
misleading 'No answer was given' while paused) and the synthetic
interactive card. The durable card now defers while the turn is live and
unanswered (isSubmitting) — the interactive card owns the question UI until
it's answered; an abandoned pause still shows its no-answer state once the
turn settles.
2. 'No answer was given' after answering: the server stamps the answer onto
the part at resume seed, but the client only received that at finalize. No
stream emission needed — the client knows the answer it just submitted:
resolveAskUserQuestionPart (replacing the plain strip on submit success)
removes the synthetic card AND stamps output/progress onto the newest
unanswered ask tool_call, seeding args from the synthetic part's question
when the streamed args were lost — mirroring the server-side
attachAskUserQuestionAnswer, so the Q&A record shows the answer the moment
the user submits.
* fix: keep the Q&A record visible while the resumed segment streams
The optimistic output stamp lives in the message store, but the SSE step
handler evolves its own cached copy of the streaming message (created at turn
start) — the first resumed event overwrites the store with that copy, wiping
the stamp, so the Q&A card blinked out during streaming and only returned at
finalize.
Render-layer fallback instead of fighting the handler's copy: submitted
answers are recorded by ask tool_call id when resolveAskUserQuestionPart
stamps the part, and AskUserQuestionCall reads the recorded answer whenever
the part's own output is missing — the record survives any message-copy churn
until finalize delivers the server-stamped part.
* feat: present Ask User as a native builtin in the tools dialog
It ships with the app and pauses the run like a first-class feature, so it
belongs with the builtins (Run Code, Web Search, Memory, ...) rather than in
the third-party plugin list — while its mechanics stay exactly a plugin's:
- BuiltinId += 'ask_user_question' (documented exception: a native TOOL, not
a capability; selection reads agent.tools, the toggle emits tool-add/remove
patches instead of a capability field)
- buildCatalog surfaces it as a builtin gated on the same signals as before
(tools capability on + the server lists the plugin, i.e. not admin-filtered)
and skips it in the plugin loop so it never double-lists
- On-theme icon: lucide MessageCircleQuestion in a teal chip via the builtin
icon map, matching the other native entries; the bespoke purple SVG and the
manifest icon field are gone
- i18n'd name/description keys like the other builtins
* feat: composer popover for answering questions (mentions-style)
Answering moves to the composer, matching the existing mentions/prompts
popover pattern: while an ask_user_question pause is live, a popover anchors
above the textarea with the question as its header, numbered option rows
(hover/click, or ↑/↓ + Enter from the empty composer), and an × to dismiss.
The main textarea doubles as the free-form answer — its placeholder flips to
'Something else...' and form submit routes the text to the paused run as the
answer instead of starting a new turn. Dismissing (× or Escape) restores
normal sends; the inline transcript surfaces stay as before (interactive card
while paused, durable Q&A record after) so the question remains visible in
history.
- findLiveAskUserQuestion (pure, spec'd): newest unanswered synthetic part
across the conversation IS the popover signal — applied on
on_pending_action, stripped on answer submit, so visibility tracks the
pause lifecycle with no extra state
- useLiveAskUserQuestion hook shared by the popover and ChatForm; dismissals
in a recoil atom so both react
- popover only mounts on the primary composer (index 0), mirroring QuoteButton
* feat: number-key selection + return glyph in the question popover
Pressing 1-9 in the empty composer picks the matching option directly,
mirroring the numbered row chips; the highlighted row shows a return-key
glyph as the Enter affordance. Same empty-composer guard as the arrow keys —
typing a free-form answer is never intercepted.
* refactor: first-class composer answer mode (useAskAnswerMode)
Replaces the bolted-on integration (inline onSubmit interception + raw
capture-phase keydown listeners on the textarea ref) with a single hook that
owns the whole answer mode: live-question derivation, dismissal + highlighted
option (shared recoil state), option selection, free-form submit routing
(submitText returns whether it consumed the submission), and keyboard
handling (handleKeyDown returns whether it consumed the key, composed ahead
of the textarea's normal handler — no more addEventListener).
The popover is now pure rendering off the hook; ChatForm wires placeholder,
onKeyDown, and onSubmit through the same instance. Deliberately scoped to the
composer rather than useSubmitMessage: starters/prompt-commands keep new-turn
semantics (and the existing job-replacement behavior while paused).
* fix: Codex round 2 — inline answer input, approval exemption, pause-time args
F1 (composer submit unreachable while paused — isSubmitting keeps Stop shown
and useTextarea eats Enter): redesigned around it, borrowing Claude Code's
AskUserQuestion semantics. The popover now owns free-form input via an inline
'Other' row (numbered last, 'Something else…'), with select-then-confirm rows
(click/arrows/digits highlight; Submit ↵, Enter, or double-click fires; Skip
dismisses). The composer returns to being a plain composer — no placeholder
swap, no submit interception; Stop keeps meaning stop.
F2: ask_user_question is exempt from the tool-approval prompt unless the
admin explicitly lists it (allow/ask/deny all win) — approving the right to
ask a question was a pure double pause; the tool is side-effect-free.
F3: the question is stamped onto the paused ask tool_call's args at PAUSE
time (attachAskUserQuestionArgs in handleRunInterrupt), so abandoned/expired/
stopped turns persist with the question intact and the record card can render
it — previously only the answer-resume path stamped args.
* fix: fold model-supplied 'Other' options into the inline free-form row
The model can generate its own catch-all option ('Other (type your own)',
value 'other'), duplicating the popover's built-in free-form row — two
other-ish rows, one pickable as a literal answer. Two layers:
- Tool description now tells the model NOT to include catch-all options (the
answer UI always offers free-form input on its own)
- splitOtherOption (pure, spec'd) folds a catch-all option that arrives
anyway out of the choice rows and uses its label as the inline input's
placeholder — conservative match (value 'other', or a label reading as a
free-form invitation), no false positives on real choices
* fix: single question surface + clean free-form-only popover
Two live-pause confusions: (1) the inline transcript card and the composer
popover both rendered — the card now defers while the popover is up for its
action, returning as the fallback surface when the user dismisses the popover
(and in contexts without a ChatContext, where the popover can't exist);
(2) an options-less question showed a pointless numbered '1 Something else…'
row — free-form-only questions now render the inline input alone, with the
'Type your answer…' placeholder (a folded model 'Other' label still wins).
* feat: the composer is the free-form answer box (like the main chat input)
While a question pause is live, the main chat textarea composes the free-form
answer — placeholder swaps to 'Something else…' (or a folded model 'Other'
label), Enter with text submits the answer through answer-mode key handling
(composed BEFORE useTextarea's submitting-lock, so the lock can't swallow it),
and the Stop button swaps to Send (enabled despite isSubmitting) per the
select-then-confirm design. The popover slims to the question header, numbered
option rows, and Skip/Submit — its inline input is gone since the composer
owns free-form now. Dismissing the popover restores normal composer semantics
(Stop button, normal sends).
* fix: Codex round 3 + real Skip semantics
- Skip now ANSWERS instead of hiding UI (danny): it resumes the run with a
decline notice ('The user chose not to answer this question.') so the model
moves on — a client-side dismiss left the run paused until expiry, a hung
turn. × / Escape remain pure dismiss (switch to the inline card surface).
- P1 (resumed approval tool indices): resumed tool_calls steps whose
tool_call id matches a seeded UNRESOLVED part now rebind to that seeded
slot instead of offsetting — the original part resolves in place (output
attaches) and no duplicate appears; message steps keep the offset, so the
text-loss fix stands. createContentIndexOffsetHandlers now takes the seed
array; resolved seeded calls are not rebind targets.
- P2 (stale selection across questions): selection state resets when the
live actionId changes; the vestigial inline-Other state ('other' selection
+ text atom) is gone — the composer owns free-form.
- P2 (Redis abort path loses the args stamp): the abort route re-stamps the
question onto the ask tool_call in the reconstructed abort content, so a
Stop-abandoned question persists with its question intact.
- P2 (malformed args crash): parseAskUserQuestionArgs normalizes untrusted
shapes (options: {} / non-string entries) instead of throwing in render.
* feat: free-form hint in the question popover footer
Left-aligned in the footer row (opposite Skip/Submit): 'Or type your answer
below' — points open-ended answering at the composer, whose placeholder
already reads 'Something else…'.
* feat: preserve composer drafts across the answer-mode swap
The answer phase gets its own draft key (ask-answer:<actionId>), passed as a
draftId override into useAutoSave — the key change itself drives the existing
save/restore machinery, so the conversation draft (or mid-run PENDING draft)
is stashed when a question pause takes the composer and restored once the
user answers, skips, or dismisses. Ask keys are exempt from the PENDING
migration branch, which would otherwise move-and-delete the stashed draft. A
half-typed answer survives reload/navigation while its question stays live.
Answer submission (option pick, free-form, skip) resets the composer via a
new non-throwing useOptionalChatFormContext, so the swap-back restores into
an empty box even outside ChatView-less render contexts (Share/search).
* fix: rebind resumed steps for ALL seeded tool call ids
The resume controller pre-stamps the user's answer onto the seeded
ask_user_question part, so the unresolved-only rebind predicate treated
it as settled and shifted the tool's re-run step to a fresh offset slot,
leaving a duplicate ask record in streamed/saved content. Tool call ids
are provider-minted per call: a resumed step bearing a seeded id can
only be the interrupted batch re-executing, so rebinding every seeded
id is always correct.
* feat: popover UX round 4 — clickable hint, collapse, click-submit, multiSelect
- Footer hint is a button that focuses the composer; reads 'Type your answer
below' (no 'Or') when the question has no options.
- Collapse (chevron) hides the popover WITHOUT closing the pause: answer mode
stays live (placeholder, Enter routing, draft key), the chat card renders
the question with a ChevronUp affordance to re-expand. x remains dismiss.
- Single-select options submit on a single click; the Submit button renders
only for multi-select.
- multiSelect end-to-end: tool zod schema + JSON definition twin, wire type,
client parse, popover check-chips, card toggles, record-card label mapping;
answer = option values joined ', '; composer Enter and the multi Submit
button both fold free-form text in with the checked values.
- Hardening from adversarial review: in-flight status guard on every submit
path (no duplicate resumes on double-click), popover locks while
submitting, collapsed mode disarms invisible digit/arrow steering, the
card shares the hook's checked state while the pause is live, the card
folds catch-all 'Other' options, record mapping is all-or-nothing to avoid
phantom labels, composer resets only when its text was consumed or the
draft machinery will restore the stash.
* feat: ask_user_question in model specs and ephemeral agents
A librechat.yaml modelSpec can now equip the tool the same way it equips
webSearch/executeCode/fileSearch/memory:
modelSpecs:
list:
- name: my-spec
askUserQuestion: true
loadEphemeralAgent pushes the tool name when the spec flag (or the
ephemeralAgent request flag, wired for parity) is set; everything downstream
is the existing persisted-agent machinery — createRun's hitlCapable gating,
graphTools injection, checkpointer attach, subagent strip, and the admin
filteredTools/includedTools kill switch all apply unchanged.
* feat: tense-aware Q&A record label (Asking / Asked)
Shorten the record card header per feedback: 'Asking' while the question is
still unanswered (abandoned/awaiting), 'Asked' once answered — replacing the
single 'Asked a question' label.
* fix: Codex round 4 — added-agent ask parity + preserve answer on failed resume
F1 (added.ts): mirror loadEphemeralAgent's ask_user_question branch in the
added-agent loader so a model spec's askUserQuestion flag (or the ephemeral
request flag) equips added top-level agents too, matching execute_code /
web_search / memory. Two load.spec cases added.
F3 (composer): submitAskAnswer now takes an onSuccess callback and
useAskAnswerMode defers clearing the selection/composer until the resume is
accepted. A failed resume (16k answer-cap 400, expired action, network error)
leaves status re-answerable, so wiping the composer up front lost the user's
only copy of a free-form answer; now it survives for trim/retry.
(F2 — a claimed Tools-capability bypass — was verified NOT reproducible:
agentRequestsAskUserQuestion matches only loaded instances/toolDefinitions/
toolRegistry, all capability-filtered; a raw tools string has no .name and
never triggers the install. Replied on-thread with the probe evidence.)
* fix: Codex round 5 — expired question exits answer mode so its message shows
An expired question (e.g. resume returns the stale-action 409) previously left
the popover open with locked controls and no explanation, because the chat
card — which carries the only 'this action expired' message — was suppressed
by the popover-open guard. Treat 'expired' as no longer active: the popover
closes, the composer reverts to normal, and the card becomes the sole surface
and renders the expired message. 'error' stays active (retryable).
* feat: group ask_user_question calls as their own category
A homogeneous group of ask_user_question tool calls now reads 'Asked N
questions' (present tense 'Asking N questions' while the turn streams) with a
question glyph and no raw-name suffix — mirroring the subagent 'Ran N agents'
category treatment, instead of 'Used N tools — ask_user_question'. Mixed
groups keep 'Used N tools' but humanize the suffix to 'Question' and show a
question icon for the ask entries (TOOL_FRIENDLY_NAME_KEYS + ToolIcon map).
A group only forms at count >= 2, so the plural is always grammatical.
Three ToolCallGroup.test cases cover homogeneous label/icon/suffix, present
tense while streaming, and the mixed-group fallback.
* fix: Codex round 6 — composer submit lock + abort stamp before emit
F7 (composer status lock): the ask submit status lived on ApprovalContext,
a React context mounted only around message content (ContentParts). The
PRIMARY answer surface — the composer in ChatForm — renders outside it, so
useApprovalContext returned the inert FALLBACK: status was always 'idle',
setStatus a no-op. The in-flight double-submit guard (round 4) and the
expired-exits-answer-mode fix (round 5) therefore never engaged for the
composer. Move ask submit status to a global Recoil atom (useAskSubmitStatus)
read/written by the composer, the popover, and the card alike, so a fast
double-click/Enter is actually blocked and expired/error surfaces on every
surface. Tool-approval status stays on the context (unchanged).
F5 (abort stamp before emit): the abort route re-stamped a paused
ask_user_question's args AFTER GenerationJobManager.abortJob had already
emitted the final SSE from the unstamped content, so a Redis/cross-replica
Stop left the live client showing an empty question until reload. abortJob
now takes an optional transformAbortContent applied to the persistable
content BEFORE the final event is built (and returned), so the live client
and the saved message agree. New abort.spec case + updated call assertions.
* feat: gate ask_user_question behind its own agent capability
Add a first-class AgentCapabilities.ask_user_question (in defaultAgentCapabilities,
on by default) so admins can enable/disable questions independently via
endpoints.agents.capabilities, exactly like execute_code / web_search — not
lumped under the generic tools capability.
- ToolService: both filteredTools predicates (definitions-only and instance
loaders) gate ask_user_question on checkCapability(ask_user_question) before
the generic tools fallthrough. When off, the tool is dropped from
toolDefinitions/toolRegistry, so run.ts's agentRequestsAskUserQuestion (which
keys on the loaded surface) declines to install it and attach a checkpointer —
the capability is enforced end-to-end at the loader, no run.ts change needed.
- Tools dialog catalog: surface the ask builtin under its own capability rather
than the generic tools one, so the UI matches the backend gate.
- Tests: ToolService capability on/off filtering + defaults membership; catalog
builtin visibility keyed on the dedicated capability.
* style: sort imports in ToolCallGroup.test (CI import-order gate)
* fix: Codex round 7 — surface ask-answer errors in the open popover
A failed answer submission (16k reject, network error) sets the ask status to
'error', which — unlike 'expired' — deliberately keeps the question active and
retryable. But the chat card that renders the error message is suppressed while
the popover is open, so a composer/popover answer failed silently. Expose an
'errored' flag from useAskAnswerMode and render a warning line
(com_ui_ask_answer_error) in the popover, so the user gets feedback and retry
guidance without having to collapse/dismiss. It clears automatically on retry
(status flips to 'submitting').
* fix: Codex round 8 — respect IME composition before submitting answers
handleComposerKeyDown runs before useTextarea's composition guard, so with a
CJK/IME keyboard the Enter that commits an in-progress composition was being
intercepted and submitting the partial answer (and the composition buffer can
leave value empty mid-compose, mis-triggering digit/arrow steering too). Bail
at the top when composing — nativeEvent.isComposing, or key==='Process' /
keyCode===229 for Safari's inconsistent reporting — mirroring the existing
composer guard so the character commits normally.
* chore: update `@librechat/agents` to v3.2.60
* 🔧 chore: Update @opentelemetry/core to version 2.9.0 and clean up package-lock.json
* feat: digit shortcuts select options when the popover has focus
Previously a number key (1..N) only selected an option from the empty
composer (handleComposerKeyDown on the textarea) — if focus moved into the
popover (a row/Skip/Submit button clicked or tabbed to), the number keys went
dead. Add handlePopoverKeyDown, wired to the popover container's onKeyDown so
it catches digits bubbling from the focused control: a digit activates its
option exactly like a click (single-select submits, multi toggles). No
highlight/Enter dance on this path — the options are buttons whose action is
the click, and intercepting Enter would fight the focused button. Gated on
active && !locked so it no-ops while a submit is in flight.
* chore: update @librechat/agents to version 3.2.61 and @opentelemetry packages to latest versions
* feat: add terms acceptance timestamp tracking and migration script
* feat: update migration script to use countUsers method for user count
* Update config/migrate-terms-timestamp.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* feat: enhance terms acceptance response to include acceptance timestamp
* fix: make terms acceptance idempotent and fail migration on partial errors
Preserve the original termsAcceptedAt on repeat accepts within a terms
cycle so retried or duplicate requests no longer overwrite the first
acceptance time. Exit the migration script with a non-zero status when
any per-user update fails so partial failures are not reported as
successful.
* style: fix import ordering in data-provider mutations
* refactor: record terms acceptance atomically to preserve first-accept time
Replace the read-then-write in acceptTermsController with a single
atomic acceptTerms method that conditionally stamps termsAcceptedAt via
an $ifNull aggregation update. This removes the TOCTOU window where two
concurrent first-time accepts could overwrite the earlier acceptance
timestamp, while still preserving an existing timestamp and backfilling
legacy accepted users.
* fix: run terms timestamp migration under system tenant context
Wrap the count, cursor scan, and per-user updates in runAsSystem so the
tenant isolation plugin does not throw under TENANT_ISOLATION_STRICT or
scope the cross-tenant migration to a non-existent tenant, matching the
other maintenance migrations.
* fix: guard terms backfill against concurrent acceptances
Add the missing-timestamp predicate to the per-user updateOne filter so
a user who accepts through the API between the cursor read and the write
keeps their real acceptance time instead of being overwritten with
createdAt. Track modified vs skipped so the summary reflects skips.
* fix: scope terms backfill to still-accepted users
Add termsAccepted: true to the per-user updateOne filter so a reset that
clears acceptance between the cursor read and the write is not re-stamped
with createdAt, which would otherwise poison the next acceptance cycle
through the $ifNull preserve in acceptTerms.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* 🔧 chore: Update `@librechat/agents` to v3.2.38 and bump related dependencies in package-lock.json and package.json files
* 🔧 chore: Upgrade `multer` dependency to version 2.2.0 in package-lock.json and package.json
* 🔧 chore: Upgrade `nodemailer` dependency to version 9.0.1 in package-lock.json and package.json
* 🔧 chore: Upgrade `@aws-sdk/client-bedrock-agent-runtime` and `@aws-sdk/client-bedrock-runtime` to versions 3.1071.0, update related dependencies in package-lock.json and package.json
* 🔧 chore: Upgrade `form-data` to version 4.0.6 and `hono` to version 4.12.25, update related dependencies in package-lock.json and package.json
* 🔧 chore: npm audit fix
* 🔧 chore: Remove unused Babel dependencies from package-lock.json and package.json
* 🔧 chore: Add '@mistralai/mistralai' to esModules in Jest configuration files
Bumps typescript 5.3.3 -> 5.9.3 across all workspaces. typescript-eslint must move 8.24.0 -> 8.60.1 too: 8.24's typescript peer was capped at <5.8.0; 8.60.1 widens it to <6.1.0.
Two errors surfaced by the newer compiler are fixed:
- api/src/rum/proxy.ts: TS 5.9 made `Buffer` generic (`Buffer<ArrayBufferLike>`), which no longer structurally matches `BodyInit`; cast the fetch body (Node's fetch accepts a Buffer at runtime).
- client usePresetIndexOptions.ts: drop a dead `|| {}` on an object spread (always truthy — flagged by the new TS2872 check).
All four package typecheck jobs + the client app typecheck pass under 5.9.3; builds (tsdown + rollup) and the rum proxy tests are unaffected.
* 🔧 chore: Update ESLint config, add import sorting script, Test Sharding, Bump `@librechat/agents`
* Change 'no-nested-ternary' rule from 'warn' to 'error' in ESLint config
* Add new scripts for sorting imports in the project
* Update lint-staged configuration to include import sorting
* Modify GitHub Actions workflows to support sharding for unit tests
* chore: remove nested ternary expressions
* refactor: Extract scale multiplier logic into a separate function in CircleRender component
* refactor: Simplify auto-refill rendering logic in Balance component for better readability
* refactor: Improve width style handling in DataTable components for clarity and maintainability
* chore: remove CircleRender component
* delete: Remove CircleRender component as it is no longer needed in the project
* chore: Bump @librechat/agents to version 3.2.31 and update Node.js engine requirement
* Update @librechat/agents dependency from 3.2.2 to 3.2.31 in package-lock.json, api/package.json, and packages/api/package.json
* Change Node.js engine requirement from >=20.0.0 to >=24.0.0 in @librechat/agents
* chore: Add import sorting check to ESLint CI workflow
* Implement a new job in the GitHub Actions workflow to verify import ordering on changed files.
* The job checks for changes in specific file types and reports any import order drift, providing instructions for local fixes.
* feat: Add granular access control to shared links via ACL system
* fix(shared-links): preserve isPublic on failed migration grants
Transient ACL failures during auto-migration permanently stranded
links — $unset ran unconditionally, removing the legacy flag that
triggers retry. Now only $unset isPublic after all grants succeed.
* fix(config): skip isPublic unset for failed ACL grants
Bulk migration unconditionally removed isPublic from all links,
even those whose ACL writes failed. Failed links then lost the
legacy marker needed for auto-migration retry. Now tracks failed
link IDs per-batch and excludes them from the $unset step.
Also adds sharedLink to AccessRole resourceType schema enum —
was missing, only worked because seedDefaultRoles uses
findOneAndUpdate which bypasses validation.
* ci(config): add jest config and PR workflow for migration tests
config/__tests__/ specs depend on api/jest.config.js module
mappings but had no dedicated runner. Adds config/jest.config.js
extending api config with absolutized paths, npm test:config
script, and a GitHub Actions workflow triggered by changes to
config/, api/models/, api/db/, or packages/ ACL code.
* fix(permissions): honor boolean sharedLinks config
SHARED_LINKS has no USE permission, so boolean config produced
an empty update payload — gate conditions only matched object
form, making `sharedLinks: false` a no-op on existing perms.
* fix(share): resolve role before creating shared link
Role lookup between create and grant left an orphaned link
without ACL entries if getRoleByName threw — retry then hit "Share already exists" with no recovery path.
* fix: Restore Public ACL Access Checks
* fix: Type Public ACL Lookup
* fix: Preserve Private Legacy Shared Links
* chore: Promote Shared Link Permission Migration
* fix: Address Shared Link Review Findings
* fix: Repair Shared Link CI Follow-Up
* fix: Narrow Shared Link Mongoose Test Mock
* fix: Address Shared Link Review Follow-Ups
* fix: Close Shared Link Review Gaps
* fix: Guard Missing Shared Link Permission Backfill
* test: Add Shared Link Mock E2E
* test: Stabilize Shared Link Mock E2E
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* chore: Update axios dependency to version 1.16.0 across multiple package files
* chore: Update express-rate-limit and ip-address dependencies to versions 8.5.1 and 10.2.0 in package-lock.json and package.json
* chore: Update mongoose and hono dependencies to versions 8.23.1 and 4.12.18 across multiple package files
* fix: Add type parameters to mongoose lean queries in accessRole and aclEntry methods
* fix: Add type parameters to mongoose lean queries in action, agent, and agentCategory methods
* chore: Update moduleResolution to 'bundler' in tsconfig.json for api and data-schemas packages
* fix: Update mongoose lean queries to include type parameters across various methods for improved type safety
* 🔧 chore: Update dependencies in package-lock.json and package.json
- Bump version of @librechat/agents to 3.1.75-dev.0 in multiple package.json files.
- Upgrade various AWS SDK and Smithy dependencies to their latest versions in package-lock.json for improved stability and performance.
* 🔧 chore: Update AWS SDK and Smithy dependencies in package-lock.json
- Bump version of @aws-sdk/client-bedrock-runtime to 3.1041.0 and update related dependencies for improved performance and stability.
- Upgrade various AWS SDK and Smithy packages to their latest versions, ensuring compatibility and enhanced functionality.
* chore: Align LibreChat with agents LangChain upgrade
- Route LangChain imports through @librechat/agents facade exports
- Update @librechat/agents to 3.1.75-dev.1 and remove direct LangChain deps
- Normalize nullable agent model params and API key override typing
- Update Google thinking config typing for newer LangChain packages
- Refresh targeted audit-related dependency overrides
* chore: Add Jest types for API specs
* test: Fix LangChain upgrade CI specs
* test: Exercise agents env facade
* fix: Clean up TS preview diagnostics
* fix: Address Codex review feedback
Addresses Dependabot advisory (uncontrolled recursion in XML serialization, DoS).
All transitive parents — mammoth, @node-saml/node-saml, xml-crypto, xml-encryption —
already accept `^0.8.x`, so this is a patch-level bump with no breaking changes.
* 🧹 fix: Prune Orphaned File References on File Deletion
Deleting a file via the Manage Files tab left its file_id in every agent's
tool_resources.*.file_ids. Stubs accumulate until the frontend dedupe keys
them as duplicates and blocks all new uploads (issue #12776).
- Add removeAgentResourceFilesFromAllAgents in packages/data-schemas: a
single updateMany/$pullAll across every EToolResources category.
- Invoke it from processDeleteRequest after db.deleteFiles so every
referencing agent is cleaned up, not just the one passed in req.body.
- Wrap the cleanup in try/catch so a stale agent update cannot mask a
successful file deletion.
* 🧼 fix: Prune Orphaned File References on Agent Update
Already-affected agents would stay broken even after the delete-time fix:
the stubs sit on the agent document until something strips them. Heal them
on the next save (issue #12776).
- Add collectToolResourceFileIds + stripFileIdsFromToolResources helpers
in @librechat/api — centralizing the tool_resources traversal used by
the controller and the follow-up migration script.
- In updateAgentHandler, check the effective tool_resources against the
files collection. When orphans are found, either strip them from the
incoming tool_resources (if the update sets them) or run the bulk
cleanup (if the update leaves tool_resources untouched).
* 🧰 chore: Add Migration to Clean Up Orphaned Agent File References
Complements the delete-time and save-time fixes by healing agents that
already accumulated orphan stubs before the upgrade (issue #12776). The
script is idempotent — re-running it on a clean database is a no-op.
- Add config/migrate-orphaned-agent-files.js following the existing
migrate-*.js convention: --dry-run by default omitted (writes by
default) and --batch-size= tuning knob. Streams agents via cursor.
- Register migrate:orphaned-agent-files and :dry-run npm scripts.
- Reuse collectToolResourceFileIds from @librechat/api so migration and
runtime share the same traversal logic.
* 🩹 fix: Address Codex/Copilot Review on Orphaned Agent File Cleanup
Refines the #12776 fix series based on automated review feedback.
- Scope save-time pruning to the current agent only. When a PATCH
carries tool_resources, strip orphans from the incoming payload and
pay the DB round-trip only then. Removes the collection-wide
updateMany previously triggered when tool_resources was absent
(Codex P2 / Copilot).
- Wrap the orphan check in try/catch so a transient db.getFiles
failure can't turn a good save into a 500 (comprehensive review #1).
- Replace Object.values(EToolResources) casts with an explicit list of
agent-side categories in both orphans.ts and agent.ts. code_interpreter
belongs to the Assistants API and isn't a key of AgentToolResources —
including it was a type lie and generated dead MongoDB clauses
(comprehensive review #3, #8).
- Export TOOL_RESOURCE_KEYS from @librechat/api and consume it in the
migration script, dropping one duplicated definition (#4).
- Cap migration results.details at 50 sample entries so the memory
footprint stays bounded on deployments with thousands of corrupted
agents (Codex P3).
- Add migrate:orphaned-agent-files:batch npm script to match the
convention set by migrate-agent-permissions / migrate-prompt-permissions
(#7).
- Add controller-level tests covering the three orphan-pruning paths:
strip from incoming tool_resources, leave alone when tool_resources
is absent, swallow db.getFiles errors and still save (#6).
- Back pre-existing "should validate tool_resources in updates" test's
file_ids with real File docs — the new pruning would otherwise strip
them, and that test is about OCR conversion / schema filtering, not
file existence. Register the File model in beforeAll so the fixture
works.
* 🩹 fix: Tighten TOOL_RESOURCE_KEYS Type and Align Migration Sample Output
Two follow-ups from the second review pass.
- Type data-schemas' TOOL_RESOURCE_KEYS as ReadonlyArray<keyof
AgentToolResources> instead of readonly string[]. Data-schemas depends
on data-provider, so the import is clean. Catches typos and aligns
with the matching export in @librechat/api — doesn't guarantee
exhaustiveness, but that's a TypeScript limitation, not a workspace
one.
- Align the migration's console output with DETAIL_SAMPLE_LIMIT: print
every collected detail (up to 50) and, when more agents were affected
than the sample size allowed, show a truncation notice. The old hard
cap of 25 meant affected agents in the 26-50 range were collected
but never shown.
* ✅ test: Add Integration Coverage for Orphan Cleanup Paths (#12776)
Exercise the delete-time and migration paths end-to-end against a real
in-memory Mongo. Catches integration bugs the isolated unit tests on
each layer couldn't.
- api/server/services/Files/process.integration.spec.js — the primary
repro: seed an Agent + File, call processDeleteRequest, assert the
file_id disappears from every referencing agent's tool_resources
while unrelated agents stay untouched. Also covers the no-op case
and confirms a failure in the new cleanup step cannot roll back the
file deletion itself.
- api/test/migrate-orphaned-agent-files.spec.js — drives the migration
module: --dry-run reports without writing, apply mode prunes across
every tool_resource category, re-running is idempotent, and
DETAIL_SAMPLE_LIMIT caps the in-memory sample on wide corruption.
Mocks only the connect helper (the spec owns the mongoose instance)
so the real migration code path — cursor, $pullAll, reduce — runs.
* 🔒 fix: Run Orphan Cleanup Migration in System Tenant Context
Codex P2 catch: under TENANT_ISOLATION_STRICT=true, the migration
throws on the very first Agent.countDocuments() because the tenant
isolation plugin fail-closes on queries without tenant context — which
makes migrate:orphaned-agent-files unusable on the exact deployments
most likely to have accumulated corruption.
- Wrap the scan/prune body in runAsSystem so queries bypass the tenant
filter (SYSTEM_TENANT_ID sentinel). The migration legitimately needs
cross-tenant visibility — this is the same pattern seedDatabase and
the S3 refresh job already use.
- Add a regression test that spies on Agent.countDocuments() and
asserts the active tenantStorage context is SYSTEM_TENANT_ID during
the call. Pins the wrap against future regressions without the
brittleness of toggling the strict-mode env var (which caches on
first read).
Note: the delete-time and save-time paths already run inside an
authenticated HTTP request where tenantStorage.run is set by auth
middleware, so the cleanup naturally scopes to the current tenant —
which is the correct behavior there since file ownership is
tenant-scoped.
* 🧹 chore: Drop Unused path Import From Process Integration Spec
Leftover from an earlier iteration that resolved the migration path
via path.resolve before I switched to a relative require. The import
does nothing now — removing it.
- Bumped the version of @librechat/agents from 3.1.67 to 3.1.68 in multiple package.json files to ensure consistency and access to the latest features and fixes.
- Updated package-lock.json to reflect the new version and maintain dependency integrity.
* chore: Update package-lock.json with new dependencies and version upgrades
- Added new dependencies for @langchain/anthropic and @langchain/core, including @anthropic-ai/sdk and fast-xml-parser.
- Updated existing dependencies for @librechat/agents, @opentelemetry/api-logs, @opentelemetry/core, and related packages to their latest versions.
- Enhanced integrity checks and licensing information for new and updated packages.
* chore: Update @librechat/agents dependency to version 3.1.66 in package.json and package-lock.json
- Bumped the version of @librechat/agents from 3.1.65 to 3.1.66 across multiple package.json files to ensure consistency and access to the latest features and fixes.
* chore: Update dompurify and fast-xml-parser dependencies to version 3.4.0 and 5.6.0 respectively
- Bumped the version of dompurify across multiple package.json files to ensure consistency and access to the latest features and security fixes.
- Updated fast-xml-parser to the latest version in relevant package.json files for improved functionality.
* chore: Update @librechat/agents dependency to version 3.1.67 in package.json and package-lock.json
- Bumped the version of @librechat/agents from 3.1.66 to 3.1.67 across multiple package.json files to ensure consistency and access to the latest features and fixes.
* 📦 chore: npm audit fix
- Bump `vite` from 7.3.1 to 7.3.2.
- Upgrade `@chevrotain/cst-dts-gen`, `@chevrotain/gast`, `@chevrotain/regexp-to-ast`, `@chevrotain/types`, and `@chevrotain/utils` from 11.1.2 to 12.0.0.
- Update `@hono/node-server` from 1.19.10 to 1.19.13.
- Upgrade `chevrotain` from 11.1.2 to 12.0.0.
- Bump `chevrotain-allstar` from 0.3.1 to 0.4.1.
* 🔧 chore: Remove `serialize-javascript` dependency from `package.json`
- Bump fast-xml-parser dependency from 5.5.6 to 5.5.7 for improved functionality and compatibility.
- Update corresponding entries in both package.json and package-lock.json to reflect the new version.
* 🔧 chore: Update dependencies in package-lock.json and package.json
- Bump @aws-sdk/client-bedrock-runtime from 3.980.0 to 3.1011.0 and update related dependencies.
- Update fast-xml-parser version from 5.3.8 to 5.5.6 in package.json.
- Adjust various @aws-sdk and @smithy packages to their latest versions for improved functionality and security.
* 🔧 chore: Update @librechat/agents dependency to version 3.1.57 in package.json and package-lock.json
- Bump @librechat/agents from 3.1.56 to 3.1.57 across multiple package files for consistency.
- Remove axios dependency from package.json as it is no longer needed.
* ✨ v0.8.3
* chore: Bump package versions and update configuration
- Updated package versions for @librechat/api (1.7.25), @librechat/client (0.4.54), librechat-data-provider (0.8.302), and @librechat/data-schemas (0.0.38).
- Incremented configuration version in librechat.example.yaml to 1.3.6.
* feat: Add OpenRouter headers to OpenAI configuration
- Introduced 'X-OpenRouter-Title' and 'X-OpenRouter-Categories' headers in the OpenAI configuration for enhanced compatibility with OpenRouter services.
- Updated related tests to ensure the new headers are correctly included in the configuration responses.
* chore: Update package versions and dependencies
- Bumped versions for several dependencies including @eslint/eslintrc to 3.3.4, axios to 1.13.5, express to 5.2.1, and lodash to 4.17.23.
- Updated @librechat/backend and @librechat/frontend versions to 0.8.3.
- Added new dependencies: turbo and mammoth.
- Adjusted various other dependencies to their latest versions for improved compatibility and performance.
* 📦 chore: bump `mermaid` and `dompurify`
- Bump mermaid to version 11.13.0 in both package-lock.json and client/package.json.
- Update monaco-editor to version 0.55.1 in both package-lock.json and client/package.json.
- Upgrade @chevrotain packages to version 11.1.2 in package-lock.json.
- Add dompurify as a dependency for monaco-editor in package.json.
- Update d3-format to version 3.1.2 and dagre-d3-es to version 7.0.14 in package-lock.json.
- Upgrade dompurify to version 3.3.2 in package-lock.json.
* chore: update language prop in ArtifactCodeEditor for read-only mode for better UX
- Adjusted the language prop in the MonacoEditor component to use 'plaintext' when in read-only mode, ensuring proper display of content without syntax highlighting.
* chore: npm audit
- Bumped versions for several packages: `@hono/node-server` to 1.19.10, `@tootallnate/once` to 3.0.1, `hono` to 4.12.5, `serialize-javascript` to 7.0.4, and `svgo` to 2.8.2.
- Removed deprecated `@trysound/sax` package from package-lock.json.
- Updated integrity hashes and resolved URLs in package-lock.json to reflect the new versions.
* chore: update dependencies and package versions
- Bumped `jest-environment-jsdom` to version 30.2.0 in both package.json and client/package.json.
- Updated related Jest packages to version 30.2.0 in package-lock.json, ensuring compatibility with the latest features and fixes.
- Added `svgo` package with version 2.8.2 to package.json for improved SVG optimization.
* chore: add @happy-dom/jest-environment and update test files
- Added `@happy-dom/jest-environment` version 20.8.3 to `package.json` and `package-lock.json` for improved testing environment.
- Updated test files to utilize the new Jest environment, replacing mock implementations of `window.location` with `window.history.replaceState` for better clarity and maintainability.
- Refactored tests in `SourcesErrorBoundary`, `useFocusChatEffect`, `AuthContext`, and `StartupLayout` to enhance reliability and reduce complexity.