`{ virtual: true }` is for modules with no file on disk, and `@librechat/client`
is a real workspace package. Registering a real module virtually let resolution
fall through to the package itself often enough to fail roughly one full-suite
run in three: the dialog then rendered through its Radix portal, putting the
file input outside the container the helper read, and `useToastContext` handed
back the real context so the size-rejection assertion watched a spy nothing
called. Both symptoms were the same mock going missing.
Drop the flag and read the input from the document, which stays correct whether
or not the stub is the module that resolves. Eight consecutive full runs clean.
The three keys lost their last consumers when the escalation UI moved onto the
queue rail and the pending steers: the two toggle labels belonged to the
overflow menu the rail replaced, and the enable hint to a settings row that no
longer explains itself that way.
UploadSkillDialog's helper read the file input out of the container `render`
returns, which only holds it while the `@librechat/client` stub is the module
actually resolved — the real OGDialogContent renders through a Radix portal
into the body, so the lookup found nothing and the suite failed roughly one run
in three under load. Query the document instead, which is correct either way.
isSubmitting flips the moment the user sends, but the start POST installs the
generation epoch a beat later. Through that window steering, send-now and
interrupt all refuse, while the composer advertised them anyway: the hint read
"Enter queues · Ctrl+Enter send now · Alt+Enter interrupt & send" when only the
queue half worked, and the send slot showed the ordinary disabled send button
because it keyed off showStopButton, which the epoch also gates.
Give the hint canControlGeneration and let it name only the default action
until the run is reachable; queueing is local, so that part is honest
throughout. Key the send slot on whether the during-run slot rendered anything
instead of on showStopButton, so the control that queues is present exactly
when Enter queues. The slot already decides between the during-run button,
Stop, and nothing, so an empty one still falls through to send.
The queue rail's escalation control had the same shape of problem: it was
gated on canSteer, so it vanished for the window after the drain starts the
next run and reappeared once that epoch landed. Show it for the whole run and
disable it whenever steering cannot reach one, which also covers the approval
pause it already handled.
Found by watching a real run: the window is normally ~250ms, but any failure
that keeps the epoch from arriving strands the composer in it.
The rebase onto dev dropped the four steering components dev had just added
escalation to, so the arm endpoint, the escalateSteer shortcut and its
translation keys all survived with nothing to act on: the shortcut resolved
and found no target.
Port the feature onto the surfaces that replaced those components rather than
reinstating them. The arm logic moves into useSteerEscalate, so the queue rail
and the in-thread pending steers escalate through identical race rules instead
of two approximations: one idempotent op flips preempt on the existing queued
item, a lost response is retried once, and every "too late" interleaving stays
an honest armed: false.
EscalateNowButton carries the shared shortcut wiring, so hovering or focusing
a row aims escalateSteer at that exact message. Both surfaces enforce the
single-interrupt invariant through escalatingSteerFamily, which covers an arm's
round trip before its own chip can report preempt.
The queued row keeps the control visible-but-disabled while paused on an
approval, which is when cutting the reply short is most wanted. The in-thread
row offers it only on an acknowledged steer: one still sending has no server id
to arm, and one already interrupting has nothing left to escalate.
Dev's "always interrupt instead" overflow toggle is deliberately not restored.
It is a global preference, and the redesign moved those to Settings, where
"Steering interrupts generation" already lives; the row keeps only what acts on
that message.
The rebase onto dev removed PendingSteerChips, so the note explaining why
the returned object is memoized named a component that no longer exists.
Bar and Queue are the memoized consumers that now depend on it.
Ctrl/Cmd+Enter did the same thing as a bare Enter whenever Enter-to-send
was on, so the chord was spent duplicating a key that was already bound
and users had only Shift+Enter to break a line.
Read the modifier as the inverse of plain Enter instead: it sends when
Enter writes a newline, and writes one when Enter sends. The rebound
path in resolveSubmitOverrideAction already resolved it this way, so
this only brings the default path in line with it.
The during-run table claims Ctrl/Cmd+Enter for the alternate send action
and resolves before the idle tail, so steering, queueing and interrupting
are untouched. Answer mode hands the newline chords to the normal path so
the free-form answer box breaks lines like the composer; Enter on a
highlighted option still confirms the selection.
Closes#12030
Putting a message back into the composer is the one path here that can destroy
text outright: the reclaimed-steer restore reports whether it took the words,
and a `true` it has not earned makes the caller drop them. Every condition it
weighs is about time passing between a click and its answer — a draft typed
since, a file staged since, a chat navigated away from, a question pause that
began, the composer unmounting — which is precisely what a component holding a
dozen other concerns makes untestable.
Moved verbatim to `useComposerRestore` alongside the unguarded `editToComposer`
it delegates to, and covered: thirteen cases, one per refusal and one per way
the guard must NOT refuse. ChatForm drops 120 lines.
Also removes ten translation keys left behind by the composer rebuild.
The composer palette derives its whole catalog from the badge-row context, and
`useToolToggle` returned a fresh object every render, so memoizing that context
value changed nothing: six new identities per render kept it invalidating on
every keystroke. Memoize the hook's own return instead.
Also removes four steer helpers, the run-end map they read and the drain re-arm
they called, none of which have had a caller since the pending steers moved into
the thread; tells the user why a queued message would not come back to an
occupied composer instead of leaving the button looking broken; and refuses a
stop once the take is already being transcribed, where it could only rewrite how
a committed take gets spent.
Dictation only ever submitted through the speech engines' auto-send
callback, which they never fire with the default setting: stop-and-send
left the transcript sitting in the composer, and a plain stop stopped
honouring Auto Send Text at all. The send is now armed on the stop and
spent once the take settles, with a per-take guard so the setting and
the button cannot both spend it. Cancel also keeps the draft on its ref
until a take is really spent, so an external transcription that was
already in flight can no longer wipe the draft it just restored.
Assistants get their unfiltered picker back — the provider check does
not recognise them, so the palette had scoped them to images and dropped
PDF support. Memory honours the user's personalization opt-out. Quote
chips are gated on the same flag as the quote button, so an endpoint
that cannot transmit an excerpt no longer shows one as staged.
The upload-file shortcut targets the palette disclosure that replaced
the attach menu, and Open Files opens the file manager dialog now that
the side panel link is gone.
Retried steers resolve "is the run over" from the conversation's
submitting state instead of the block's unmount: navigating away from a
live run unmounts it too, and queueing there sent the same words twice.
Composer hints follow the Enter-to-send setting rather than always
naming Enter as the send key.
Send now fell back to the message the row was rendered with, so a click
landing just after the drain sent the same words twice. An admin override
replaced a reasoning parameter instead of merging into it, dropping the
options the control needs to render at all. The landing screen released
its lift only when the popup closed, not when the composer unmounted.
Chip widths move into state, concurrent steer retries each keep their own
frame handle, and starting a stream is wrapped so a throw cannot leave the
composer stuck generating.
Trashing a queued message removed it even when the composer refused to
take the text back, losing it. Escape during a drag left the reorder
applied, since rows move as the pointer crosses them. The palette had its
own copy of the favourites hook and lost the cap toast with it.
Also removes the cancel and reclaim hooks, which lost their consumer with
the in-flight steer menu, and covers the queue actions, the picker's tool
routing, useAttachExisting and the skill scoping gate.
The virtualized list added its own grid role and tab stop between the
listbox and its options, and the highlight followed a position rather than
a row, so starring one left aria-activedescendant naming a header. Hidden
chips stayed tabbable during a recording, the effort track answered no
arrow keys, and dictation was never announced.
The tool catalog was also rebuilt on every keystroke, from unstable
capabilities, an unmemoized context value and a row model derived while
the palette was closed. Every dictation take leaked an audio context.
Converting a steer sorted the whole queue by creation time, undoing a
reorder made from the rail and changing which message sent next. Each
converted item is placed at its own position instead.
Retries resolved through mutate callbacks, which react-query drops once
the observer unmounts. The pending block unmounts when the run ends, so a
chip could sit on "sending" for the rest of the conversation.
The redesign deleted the attach menu specs along with their components,
but that logic moved into hooks and went untested, which is how the code
environment upload regressed. Restores those cases and adds the palette
row model, the entries it is offered, and chip packing.
The rail is the send order, and it was fixed at the moment each message
was written. Rows carry a handle now; the keys work without a pointer and
announce the new position.
React re-inserts every row a starred entry passes, and a re-inserted node
drops the transition it was about to run, so starring jumped while
unstarring animated. The move is played after the redraw instead.
Starring a row rearranged the list without telling react-virtualized to
re-measure, so every row below it was drawn at the height of whatever used
to sit at that index until the palette was reopened.
Only the provider row stays out. Text, file search, code environment and
the SharePoint mirrors sit behind one row that opens in place. Searching
still reaches all of them by name.
They were only reachable by dropping a file on the composer. The menu and
the drop router now resolve the same allowances, so an ordinary chat can
upload to file search or the code environment.
The abort listener also fires on navigating away while the run continues
server side. Sweeping pending chips there stranded a steer the server had
already acked and sent it a second time. selectLocalSteersForQueue takes
a statuses argument so abort sweeps failed only.
Four useSteering tests went tautological once the default flipped to
queue. Seeding duringRunDefaultAction keeps them covering the guard they
were written for.
Replaces the badge row, attach menu and tools gear with a single
searchable palette: tools, skills, MCP servers and recent files, with
favourites persisted server side. Active tools become removable chips
that pack widest first and wrap to their own row when they need to.
Also adds the thinking effort slider, rebuilds dictation with a waveform
and a real cancel, and drops AudioRecorder now that the bar covers it.
* 🪺 fix: Keep Preempt-Abandoned Siblings Nested, Make Message Tree Order-Robust
* 🔗 fix: Sever Cycle Back-Edges So the Returned Message Tree Is Acyclic
* 🧪 fix: Satisfy TFile in buildTree Spec fileMap Fixture
* 🌲 fix: Assert Repaired Trees in convoStructure Specs, Uncharge Self-Parent Edges
* 📌 feat: Identity-Stable Sibling Selection Across Background Tree Churn
* 🎭 test: E2E Coverage for Thread Fold and Sibling Selection Invariants
* 🔑 fix: Treat Newest-Sibling Re-Key as Hydration, Not a New Branch
* 🧭 fix: Rebind Sibling Selection Per Parent, Detect Appends by Membership
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.
* 🔒 fix: Single-Flight MCP OAuth Token Refresh per User/Server
Concurrent refresh-token redemptions (tool-call 401, ping, reconnect
retries, expired-token reads) each replayed the same stored refresh
token at the OAuth token endpoint. RFC 9700 reuse detection treats the
replay as theft and revokes the entire grant family, forcing manual
re-consent every access-token expiry.
MCPTokenStorage.forceRefreshTokens is the choke point every refresh
path converges on; it now single-flights redemptions per
(tenantId, userId, serverName) so concurrent callers share one wire
call and receive the same rotated result. The refresh token is re-read
from storage inside the locked execution — never from a caller
snapshot — so a redemption starting after another refresh completed
uses the rotated token instead of replaying the consumed one.
Fixes#14583
* 🧪 test: Isolate Single-Flight Keys per Test via Unique Server Names
* 🔒 fix: Evict Stalled Refresh Slots, Decouple Waiter Aborts from Shared Redemption
Codex review round 1:
- A redemption that never settles no longer wedges the single-flight
slot until process restart: a stale-entry timer evicts the map entry
so later refreshes start fresh, while existing waiters keep their
promise.
- Caller AbortSignals no longer thread into the shared redemption. An
impatient waiter (silent refresh's short timeout) resolves its own
wait with null via a per-waiter race; the shared wire call proceeds
for everyone else, bounded by transport timeouts plus eviction.
* 🔒 fix: Abort Stalled Refreshes Before Slot Release, Hook Cache Invalidation to Redemption
Codex review round 2:
- The stale timer now aborts the wedged execution instead of deleting
its slot; the slot frees only once the execution has settled, and an
abort guard before the token-endpoint call stops a woken pre-wire
stall from replaying a refresh token a successor already rotated.
- New onRefreshSuccess hook runs inside the shared redemption after
rotated tokens persist, so the silent-refresh path's mcp_get_tokens
cache invalidation fires even when the initiating waiter timed out
before the redemption completed.
* 📝 docs: Record Post-Dispatch Abort Recovery Rationale on Stale-Refresh Valve
Unresolved {{LIBRECHAT_USER_*}} header templates leaked literally to
upstream providers when user context was missing at resolution time
(e.g. async title generation racing client disposal), letting a gateway
trust LibreChat's own template syntax as an account identity.
resolveHeaders now takes an opt-in stripUnresolved flag that blanks any
resolvable-but-unresolved LIBRECHAT_USER/BODY/OPENID placeholder, enabled
at every final resolution boundary (resolveConfigHeaders, model fetches,
Google init, summarization overrides, azureAssistants init). Staged
passes that resolve again later with more context are left untouched, as
is the async-resolved {{LIBRECHAT_GRAPH_ACCESS_TOKEN}} and unknown names.
titleConvo now resolves headers from the req captured at entry instead of
re-reading this.options.req, which disposeClient nulls concurrently.
Fixes#14580
The lazy BookmarkNav's Suspense resolution commits during waitFor's
polling, outside any act scope, so its follow-up render work lands in
React's real scheduler as a macrotask. The single empty async act added
in #14071 only drains microtasks and the act queue, so on slow Windows
shards that work can still be pending when baselines are captured. The
next act flushes pending root work wholesale, so the first stream tick
carries the leftover pass and inflates the tag counter (Expected: 1,
Received: 2). Flush full event-loop turns inside act until two
consecutive turns add no renders, then capture baselines.
* 💬 feat: Interim Progress Card for Streaming ask_user_question Calls
* 🔍 fix: Match Progress Card Against Every Live Ask Pause, Not Newest Only
* ⏳ feat: Hold Streaming Cursor Under Answered Question While Resume Is In Flight
* 📦 chore: Upgrade react-router-dom to v7.18.2 (security)
Fixes GHSA-wrjc-x8rr-h8h6 (open redirect via backslash in Link/useNavigate,
CVE-2025-68470 bypass) and GHSA-337j-9hxr-rhxg (deserializeErrors constructor
injection). Neither has a 6.x patch; v7's react-router-dom is a shim
re-exporting react-router, so all existing imports work unchanged.
- vite manualChunks: match react-router so the routing chunk still captures
the router (v7 moves all code out of the react-router-dom package)
- jest: add test/polyfills.js (TextEncoder/TextDecoder + minimal Request);
v7's CJS bundle constructs TextEncoder at module scope and builds a Request
per navigation, neither exists in jsdom
- auth specs: v7 types drop the synthetic default export; use a namespace
import and mark the mock factory __esModule so the useOutletContext spy
patches the object components actually read
- isSafeRedirect: reject backslashes as defense in depth for the same
open-redirect class the router patch addresses
* 📦 chore: Regenerate stale bun.lock
bun.lock predated months of package.json drift and still pinned
react-router 6.30.3. Regenerated with bun install --lockfile-only so bun
installs match current manifests, including react-router 7.18.2.
* 🗂️ fix: Commit project-chip URL updates synchronously under router v7
v7 wraps router state updates in React.startTransition unconditionally, so
the chip's paired updates tear: the conversation draft (Recoil) commits
synchronously while the ?projectId removal defers. ChatRoute's
draftProjectMismatch re-init sees draft != URL in that window and restores
the removed project. The flushSync navigate option commits both in one pass,
matching v6 ordering. Caught by the projects e2e specs.
* 🧹 chore: Drop unused banner-query spy variable in Registration spec
Pre-existing warning, but the changed-files eslint gate runs with
--max-warnings=0 so it blocks this PR. The spy call stays; only the
never-read variable goes.