The popover carried both a chevron and a close button. Both left the
question pending and both moved it into the chat, so their only real
difference was invisible: the chevron kept the composer wired up as the
answer box while the close button released it. Two icons for one visible
outcome, with the meaning hidden in a placeholder change somewhere else.
Collapsing now does both jobs at once. Moving a question to the chat hands
the composer back for normal messages, and the card's chevron re-arms it,
so the popover's visibility and the composer's role can never disagree.
Escape maps to the same path and the dismissed-ids state is gone. Both
chevrons get a tooltip and a matching label, since no glyph conveys this.
Also fixes two behaviours found along the way: the card's Skip was a
silent no-op once the question left the popover, because it routed through
a helper gated on answer mode instead of the live pause, and clicking a
multi-select row moved the keyboard highlight, leaving a row painted as
selected after it was unchecked.
The popover and the chat card rendered the same question two different
ways: numbered rows in one, wrapped chip buttons in the other, and the
panel was painted with hardcoded white/gray instead of theme tokens.
Both surfaces now render one AskOptions list, so numbering, check badges,
rounding and hover states match wherever a question appears. The card
groups its prompt with the description, styles the answer box like the
composer, and gains a Skip button beside Submit so declining does not
require the popover. While the popover holds the question the card keeps
a hidden copy of itself in the thread, reserving the exact space the card
will need so nothing reflows when the question moves back.
Wrap a state update in startViewTransition (with a synchronous flush) so
elements sharing a view-transition-name animate their position, size and
shape between two states instead of swapping.
The composer band gets its own named, opaque snapshot group stacked above
the morphing element: without it a surface travelling between the thread
and the composer paints over the input and bleeds through the strip below
it. Falls back to a plain update where the API is unavailable and for
reduced-motion users.
The button lived inside the scroll container with sticky/float
positioning, so horizontal scrolling dragged it through the command
text. It now sits absolutely in a non-scrolling wrapper around the
scroller and stays in the corner on both axes.
The preview only faded while the full prompt popped in with the height
expand. The preview now slides down as it fades toward the incoming
text, and the expanded prompt slides down into place from under the
header, so the disclosure reads as one continuous movement. Both
transforms sit behind motion-reduce.
The truncated prompt text was tertiary and nearly invisible on the
panel background; it reads as secondary now, and the header hover uses
surface-secondary so it registers against the panel surface.
Place rows were the only vertical without a destination. Each row now
links out to the user's native maps app: Apple devices (detected via
the existing isMacPlatform check) get maps.apple.com with the query
and coordinates, everything else gets a Google Maps search link. Both
are keyless https links that degrade to a web map, so a misdetected
platform still lands somewhere useful. Rows pick up the same hover
treatment as the other linked results.
Image, shopping, and place results were only visible after expanding
the web search row, and disappeared entirely inside a collapsed tool
group. The strips moved into a shared verticals module that renders
in two spots following the attachment pattern: always below the row
for an ungrouped search, and hoisted below the group (visible while
collapsed) for grouped calls, where the row suppresses its own copy
via hideAttachments. The disclosure panel is back to sources only.
Serper vertical results were parsed into the artifact and dropped.
The expanded panel now renders them when present: an image strip of
rounded thumbnails at true aspect ratios (sized from thumbnail
dimensions, no layout shift), a shopping card carousel with product
image, title, price and store, delivery note, and star rating, and a
place list with name, category, address, and rating. All link out,
use semantic lists with localized labels, and cap item counts.
Expansion now keys off any renderable content, so a verticals-only
search still opens.
The inline answer card read as assistant prose sitting inside the
source list. The panel is back to sources only; an Info trigger at the
right edge of the row (hover-revealed like the chevron, focus-openable
for keyboard) opens a hover card with the search query, the source
count, and the provider answer box when present. WebSearch now
receives the tool call args to surface the query.
The panel only showed favicon, title, and domain while the search
artifact carries much more. Source rows now include the result snippet
(clamped to two lines) and date under the domain, and a Serper answer
box renders as a lead card above the list with its title and snippet.
Streaming behavior is unchanged; the answer box is attachment-only
since it never carries citation anchors.
The expanded source panel sat flush against the 22px favicon stack,
which overflows the 20px row. The panel now opens with a 6px top
margin while the row keeps the shared my-1 rhythm.
set_memory and delete_memory rendered through the generic tool card,
reading "Ran Save Memory" with raw parameter chips like an unknown MCP
tool. A new MemoryCall part gives them first-class rows: "Saved
memory" / "Deleted memory" with the key as a quiet subtitle, the
Brain icon, and an expanded panel that mirrors the memory attachment
display (bold uppercase key over the stored value, or a deletion note),
with error output shown inline when a write fails.
The dual old/new gutters left one column blank on every add or delete
row, reading as a stray number floating far from its marker. Each row
now shows the one relevant line number (old for removals, new
otherwise) right beside the +/- marker, and the scroll container is
vertical-only since lines wrap.
The row offered an expand affordance whenever raw output existed even
when no sources parsed, opening an empty panel. Expansion now keys off
parsed results, which also covers attachment-only sources that arrive
without text output, and collapses if results disappear.
Rebuild MemoryArtifacts on the shared disclosure row (useExpandCollapse
grid animation, hover-reveal chevron, h-5 header) in place of its
bespoke ResizeObserver height animation, and simplify the expanded
panel: section headings dropped in favor of bold memory keys, a single
p-3 inset instead of double padding, and the now-unused heading
translation keys removed.
Replace the highlighted-text diff in file authoring output with a
line-based renderer: red and green tinted rows with +/- gutters,
old/new line numbers when hunk headers carry them, and diff noise
stripped (file headers, streamed old_text/new_text markers, bare @@
separators become dividers). The code window header shows the file
name with +N/-N change stats next to Copy Code, and file authoring
rows adopt the path-first arg parsing and shared panel spacing.
Group headers now describe the activity performed: homogeneous search
groups read "Searched the web" or "Searched web and files", mixed
groups read "Ran N actions" with per-label repeat counts (Create File
x2) and a failure tally in the detail line. Generic tool rows resolve
their label through getToolDisplayLabel, the OAuth trust warning moved
into a shared ToolAuthWarning that a group renders once instead of per
call, and the group body is a flex column so row margins stop
collapsing unevenly between rows.
Map skill, read_file, create_file, edit_file, set_memory, and
delete_memory to localized display names so summaries stop showing raw
tool ids, and give the memory tools the Brain icon the Memories panel
already uses. read_file and skill rows parse the arg the backend
schemas actually emit (path, with file_path as a legacy fallback;
skillName) and fall back to a generic noun instead of rendering an
empty name in their labels.
Extract the hover-reveal chevron and expanded panel spacing into
disclosure.ts and align every tool activity row on the same layout:
h-5 header, my-1 rhythm, full-width click target, and a 200ms chevron
rotation. Rows rendered through ProgressText now stretch across the
message column (the absolute content layer was shrink-wrapping to the
text), and the copy buttons in thinking and output panels anchor to
the row instead of the panel corner.
- Replace the subagent dialog with a right-side panel that shares the
artifacts slot (currentSubagentRunId + subagentPanelVisibility atoms,
a subagentRunsState registry, and a useOpenRightPanel cross-clear so the
two panels are mutually exclusive)
- Rebuild the inline subagent card to be tool-consistent, preview a single
live/result line, and open the panel (auto-focusing on stream arrival)
- Relocate the originating prompt to a collapsed, smoothly-animated
disclosure under the panel header
- Gate the Thoughts copy button to expanded state and move the floating
bar to a scroll-out reveal, matching the code-block pattern
- Fix the reasoning expand/collapse jump by animating the trailing gap
inside the grid track instead of an expand-only margin
- Treat reasoning (Thoughts) as transparent to tool grouping so interleaved
thoughts fold into the tool group instead of splitting it
- Group a lone tool call that has reasoning (e.g. a skill) so it gets the
same collapsible chrome as multi-tool groups
- Count only real tool calls for the group header; add a reasoning indicator
and a singular 'Used 1 tool' label
- Rework the in-group Thoughts panel: tool-row sizing, rounded content,
header copy button, and a floating collapse/copy bar
- Round the floating thinking-bar buttons
- Refine the agent handoff row and instructions panel with consistent
spacing and a copy affordance
* 📉 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
Skill priming fanned out one unbounded batch upload per cold skill,
bursting through codeapi's per-user upload limiter (30 per 5 min).
Failures degraded silently: nothing persisted, every turn re-burned
budget, and handle_skill reported success with no files mounted.
- Bound batch uploads to 3 process-wide slots across both prime paths
- Single-flight primeSkillFiles per (skill id, version)
- Retry a 429 once per Retry-After, capped at 15s, fresh streams
- handle_skill now tells the model when bundled files are unavailable
- Warn on fulfilled-null primes in primeInvokedSkills
* 🪺 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