mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
2264 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
728fc1276e
|
🔒 fix: Bound /files/usage TTL Hold Instead of Clearing It (#14470)
* 🔒 fix: Bound `/files/usage` TTL Hold Instead of Clearing It `POST /files/usage` marks queued attachments so the 1-hour upload-window TTL cannot reap them before the client queue drains. It did this by calling `updateFilesUsage`, which unsets `expiresAt` outright, turning every touched upload into a permanently retained file. The client queue is ephemeral browser state, so this also leaks in normal use: a closed tab or cleared queue leaves nothing referencing the files, but their TTL is already gone. The same mechanism let an authenticated user pin arbitrary owned uploads indefinitely, and the route was excluded from the file limiters, so the touch was entirely unmetered. Make the operation match its intent, a renewable hold rather than a release: - Add `extendFilesTTL`, which pushes `expiresAt` forward by a bounded window in a single owner-scoped `updateMany`. Two filter guards keep it safe under client-supplied ids: `$exists: true` so an already-released file never has a TTL re-added (that would schedule a live file for deletion), and `$lt` so a hold only ever moves the deadline later. The owner scope is a required argument, so an unscoped call is a no-op rather than a cross-user update. - `handleFilesUsageRequest` now holds for 24h instead of clearing, and no longer increments `usage`, since a queue touch is not a send. The real release still happens at drain, where `updateFilesUsage` marks the files used against an actual message. - Give `/usage` its own per-user limiter. Keeping it off the upload quota was intentional, leaving it unmetered was not. Abandoned queues are now reaped on schedule, and a replayed touch can only ever re-assert the same bounded window. * 🔒 fix: Anchor the `/files/usage` hold to upload time Codex review on |
||
|
|
ea643e8c9c
|
🔗 fix: Render Shared Links Containing Steers (#14480)
* 🔗 fix: Render Shared Links Containing Steers
The /share/:shareId route mounts outside AuthContextProvider, so any
useAuthContext() on that tree throws and the whole page is replaced by the
route error boundary. SteerPart called it directly, and the MessageIcon tree it
renders reaches Endpoints/Icon, which called it too - so fixing only the first
still died on the icon.
Both now read the user atom instead. AuthContextProvider mirrors the user into
it, so authenticated rendering is unchanged, and the share route reads
undefined rather than throwing.
The two crash sites were invisible because the spec mocked both
~/hooks/AuthContext and MessageIcon. Both mocks are gone: the test seeds the
atom and renders the real icon tree, with a case covering the share route
having neither an auth context nor a user.
* fix: sort test imports and assert the real avatar title
The worktree has no node_modules, so the lint-staged sort-imports hook never
ran on the first commit and CI caught the drift.
The icon assertion also used the wrong value: Endpoints/Icon derives the title
from user.name ?? user.username, so the seeded user renders 'Danny', not the
username.
* fix: keep viewer identity off shared steer avatars
store.user is app-wide and survives navigation, so a signed-in viewer opening a
share link still has an identity in state — reading it for the avatar put the
viewer's face on the sharer's steer. The shared branch now renders the generic
avatar, mirroring Share/MessageIcon, while the label guard already handled the
text.
Also fixes the share test, which passed undefined into a defaulted parameter
and so seeded a user anyway, testing the signed-in path it claimed to exclude.
|
||
|
|
52fcc51b36
|
🌍 i18n: Update translation.json with latest translations (#14460)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
|
||
|
|
250aca375a
|
🔗 fix: Resolve MCP Tool-Key Boundary Against Configured Server Names (#14448)
* fix: resolve MCP tool-name delimiter collision at invocation time
MCP tool keys are identified internally as `${rawToolName}${mcp_delimiter}${serverName}`
(delimiter `_mcp_`). Several call sites parsed this back apart with a naive
`toolKey.split(Constants.mcp_delimiter)`, assuming the delimiter occurs exactly once.
When the raw upstream tool name itself contains the delimiter substring - which
happens whenever it's exposed through a gateway that prefixes aggregated tool names by
server (e.g. a gateway's own "gitlab-get_mcp_server_version" for GitLab's
"get_mcp_server_version" tool) - the combined key has the delimiter more than once.
`.split()` then produces more than two segments, and destructuring
`[toolName, serverName]` silently keeps only the first two, yielding a bogus server
name that matches no configured server. Tool listing still worked (a different code
path builds keys directly without re-splitting), but invocation failed with
`Tool {name} not found`, and `filterAuthorizedTools` rejected such keys outright as
malformed.
Add `splitMCPToolKey`, which splits on the *last* occurrence of the delimiter instead:
the server-name half is always LibreChat's own normalized suffix (guaranteed not to
contain the delimiter), while the raw tool-name half is untrusted and may legitimately
contain it. This matches `.split()`'s result whenever the delimiter occurs once, and
correctly resolves the collision case. Update the four call sites that parsed this
manually (`handleTools.js`, `MCP.js`, `mcp.js` controller, `filterAuthorizedTools` in
`v1.js`) plus one in the client (`useVisibleTools.ts`) to use it.
Fixes #14440
* fix: resolve MCP tool-key boundary against configured server names
splitMCPToolKey moves to librechat-data-provider so the client and backend
share one parser, and takes the configured server names when the caller has
them: the longest name the key actually ends with wins, which is exact.
Position alone cannot identify the boundary because both halves may contain
the delimiter. lastIndexOf alone fixes gateway-prefixed tool names but
regresses servers whose own name contains it, which ToolService.spec.js
already covered; the last-delimiter path now only serves as the fallback for
callers with no configured set.
Also converts the remaining first-occurrence parsers that the delimiter fix
missed - mcp/auth.ts (custom user vars silently unresolved), mcp/oauth/events.ts,
agents/initialize.ts, and the three client parsers that labelled tool calls
with the wrong server.
* fix: keep client tool-call labels on first-delimiter parsing
The three client parsers had deliberate, tested first-delimiter semantics
(ToolCall.test.tsx asserts the full server name for 'foo_mcp_bar' and the
synthetic 'oauth_mcp_server' call), and the client has no configured server
list in scope to resolve the boundary exactly, so they are left as they were.
Threads the configured names into the event-driven definition loader so it
resolves the same boundary as the authorization filter that admits the key,
and documents the one case that stays undecidable without provenance.
* fix: resolve tool-key boundary against all configured servers
resolveConfigServers only returns lazily-initialized config overrides -
ensureConfigServers skips unmodified YAML servers - so on a stock deployment
the known-name list was empty and suffix resolution never engaged. Adds
resolveMcpServerNames, which keeps every configured server in the normalized
form tool keys carry, and uses it at the loading, auth-map and definition
sites.
Background-tool eligibility now resolves against all configured names before
testing ephemeral membership, so a non-ephemeral server whose name ends in an
ephemeral one is no longer misclassified, and useVisibleTools resolves against
the server map it already receives.
* fix: use resolved server provenance and one app-config read
createMCPTool now uses the serverName loadTools already resolved for the key
and only parses as a fallback, so an unmodified YAML server whose name
contains the delimiter no longer resolves to the wrong server for auth,
reconnection and callTool.
resolveMcpServerContext derives config servers and all configured names from
a single getAppConfigForRequest, replacing two independent lookups on the
chat startup path, and degrades to empty like resolveConfigServers instead of
aborting tool loading when the config lookup fails.
* chore: drop unused resolveConfigServers import
* fix: forward server provenance on the all-tools path and read config once
createMCPTools builds each toolKey from the server name it already has but did
not forward it, so the sys__all__sys path re-derived it by parsing and bound
an unmodified YAML server whose name contains the delimiter to the wrong auth
and invocation context.
loadAgentTools now resolves the MCP server context once and threads it into
loadTools, replacing the second app-config read it had introduced on the
non-event-driven chat startup path.
* fix: carry resolved MCP server name through tool classification
definitions.ts resolves the server for each key and then dropped it when
building loadedTools, so buildToolClassification re-derived it with a
last-segment split and recorded 'Workspace' for a server configured as
'Google_mcp_Workspace'. The resolved name now rides along on the tool
instance and classification prefers it over re-parsing.
* fix: consume carried server name when extracting MCP servers
extractMCPServers re-derived the name with a last-segment split, so a server
configured as Google_mcp_Workspace resolved to Workspace and its instructions
were silently omitted. Prefers the name carried on the tool definition
instance, falling back to the split.
* fix: fail closed on ambiguous MCP keys when persisting server names
Persisted mcpServerNames grant agent-scoped access to a DB server by name
(ServerConfigsDB.getAccessibleServers), so a wrong guess exposes an unrelated
server to everyone who can view the agent. The last-segment split turned
search_mcp_Google_mcp_workspace into 'workspace'; such keys were previously
rejected outright at agent save, so admitting them opened this path.
Derives a name only from unambiguous single-delimiter keys. This is #12250's
guard moved to the boundary it was actually protecting, instead of blocking
tool admission.
* fix: keep DB server access for multi-delimiter tool keys
The fail-closed guard was wrong for the case this PR exists to fix. This index
only grants DB-backed servers, and DB names are slugs that cannot contain the
delimiter (generateServerNameFromTitle strips underscores), so the trailing
segment is always the real server for them - dropping it cost every consumer
of a gateway-prefixed tool their shared-agent access.
Also gates the MCP server-context lookup on the filtered MCP set, so an agent
with no MCP tools no longer pays an app-config read on startup.
* fix: resolve tool-call display names without breaking OAuth calls
The display parsers could not use the shared boundary parser because their
tested behavior depends on first-delimiter semantics. That constraint only
applies to synthetic MCP OAuth calls, whose tool half is always exactly
'oauth', so everything after the first delimiter is the server even when the
server name carries one.
splitToolCallName special-cases that form and defers to splitMCPToolKey for
real tool keys, so a gateway-prefixed tool now renders its own name and
server while oauth_mcp_foo_mcp_bar still resolves to foo_mcp_bar.
* fix: persist resolved MCP server provenance on agents
Deriving mcpServerNames from the tool key cannot tell a config server's
trailing segment from a real DB server name, so a config server named
a_mcp_b indexed an unrelated DB server b and shared the agent's viewers into
it. Neither string rule works: the suffix guess exposes, and failing closed
drops legitimate DB access for gateway-prefixed tools.
filterAuthorizedTools already resolves each tool's server against the merged
registry config, so it now collects those names and create, update and
duplicate persist them. No extra registry queries: the update path unions the
newly resolved names with what the agent already had, and duplicate replaces
the copied list rather than inheriting the source's servers.
Display parsing also takes the configured names, so a real tool call on a
delimiter-bearing server renders the right server and icon.
* test: teach MCP hook mocks about useMCPServerNames
Three specs mock ~/hooks/MCP with a hand-listed factory, so adding the hook
to ToolCall made useMCPServerNames undefined under test and every render
threw. Returns a stable array so the mock cannot perturb render counts.
* fix: rebuild agent MCP server index from surviving tools
Unioning the prior names kept a server indexed after its last tool was
detached, so viewers of a shared agent retained agent-scoped access to it.
The index is now rebuilt from the tools that survive the edit: a prior name
carries forward only while some retained tool still resolves to it, using the
agent's own persisted names as the candidate set, and the rebuild runs on any
tool change rather than only when a new MCP tool is added.
* fix: keep duplicate indexes on registry fallback and harden the oauth split
Duplication blanked mcpServerNames when the registry was unavailable, because
filterAuthorizedTools grandfathers the source's tools without resolving them -
the copy kept tools it could no longer resolve. Source names now carry forward
for the tools that still point at them.
splitToolCallName also treated any oauth_mcp_ prefix as a synthetic OAuth
call, so a genuine upstream tool by that name resolved to the wrong server. A
configured server name now decides when one matches, since a real key always
ends in its server, and the prefix only breaks ties for unconfigured servers.
* fix: thread configured server names through display parsing
parseToolName and getMCPServerName resolved context-free, so a configured
server whose name contains the delimiter showed the wrong server in grouped
tool summaries and subagent tool labels, and stacked icons missed its entry in
the icon map. Both take the configured names now, supplied by the components
that render them.
Adds the hook to SubagentCall's mock factory: the spec renders the real
component, so an unmocked useMCPServerNames would reach the query with no
provider.
* test: cover the auth-map boundary, server provenance and context fallback
Adds regression coverage for three behaviors this PR changed that no test
exercised: customUserVars resolving under the right plugin key for a
gateway-prefixed tool name (the failure that made these tools loadable but
unusable), the resolved server name reaching createMCPTool instead of being
re-parsed, and resolveMcpServerContext degrading to empty rather than
aborting tool loading when the config lookup fails.
Each was checked against a mutated source to confirm it fails when the
behavior is broken.
* fix: normalize server-name candidates and cover the boundary guard
Tool keys embed normalizeServerName's output while the config is keyed by the
raw name, so callers passing raw keys never matched a server whose name needs
normalizing and silently fell back to the last delimiter. filterAuthorizedTools
now maps normalized names back to their config key, and createMCPTool
normalizes its candidates.
Adds the cases an audit found surviving mutation: a configured name that is a
bare but not delimiter-aligned suffix must not match, an empty candidate list
behaves as no list, and splitToolCallName still falls back to the oauth prefix
when a list is supplied but nothing in it matches.
* fix: keep resolved server names when a non-owner retains MCP tools
The shared-agent path keeps an agent's existing MCP tools verbatim but supplied
no mcpServerNames, so persistence re-derived them and reduced a configured
server like Google_mcp_Workspace to Workspace - which ServerConfigsDB then
treats as a DB server, granting the agent's viewers access to an unrelated one.
Carries the existing resolved names across instead, and clears the index on the
owner path where every MCP tool is removed.
* fix: preserve resolved MCP names for every tools update
extractMCPServerNames was reachable from any caller that writes tools without
mcpServerNames - the Action edit path does exactly that - so a configured
Google_mcp_Workspace was reindexed as Workspace and ServerConfigsDB granted
shared-agent viewers an unrelated DB server by that name.
updateAgent now rebuilds the index from the agent's own resolved names: one
carries forward while a retained tool still resolves to it, and only keys
matching none of them fall back to derivation. Callers are safe by default
rather than by remembering to pass the set.
normalizeServerName moves to librechat-data-provider so the client can match
its candidates against tool keys, which embed the normalized form; the icon map
is keyed the same way since it is looked up with a parsed server name.
* refactor: move MCP context resolution into packages/api
New backend logic belongs in the TypeScript workspace per CLAUDE.md, with /api
kept to a thin wrapper. resolveMCPServerContext now lives in
packages/api/src/mcp/context.ts and takes ensureConfigServers by injection,
since the registry accessor is still legacy-only; the /api function is reduced
to loading the request app config and translating failures into the empty
degrade it already promised.
* test: teach the MCP service mock about resolveMCPServerContext
The spec mocks @librechat/api with a hand-listed factory, so moving the
resolver into that package left it undefined and the wrapper degraded into its
own catch, returning empty config servers. The stub mirrors the real resolver
so these tests still cover what the wrapper owns - loading the request config
and degrading on failure - while the resolution logic is unit-tested in
packages/api.
* fix: only persist an authoritative MCP server index on update
Assigning the resolved set unconditionally pinned the index to [] whenever
nothing authoritative was available - a legacy agent holding MCP tools with no
stored mcpServerNames - which suppressed updateAgent's derivation and stripped
agent-scoped access to its DB-backed server.
The field is now supplied only when the result is authoritative: names were
resolved, or no MCP tool survives so the index genuinely is empty. The
retained-tools branch likewise leaves it unset when the agent has none stored.
---------
Co-authored-by: Jens Schumann <schumajs@gmail.com>
|
||
|
|
74f46f90a1
|
🗺️ chore: Bump PostCSS to 8.5.18 to Patch Source Map Traversal (#14463)
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. |
||
|
|
a53936d273
|
🧭 test: Cover Agent Handoffs End to End (#14428)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* test: cover agent handoffs end to end * style: sort handoff imports * fix: normalize missing agent handoff edges * chore: update package dependencies and versions in package-lock.json and package.json * chore: bump agents SDK |
||
|
|
d8427ffc5e
|
🛂 test: Cover Tool Approval Workflows End to End (#14427)
* test: cover tool approval workflows end to end * fix: preserve tool approval state across resume * fix: preserve agent context in mock stream responses * fix: preserve nested approvals in collapsed groups |
||
|
|
f3159f9891
|
🧩 fix: Harden Agent Skill Lifecycles End to End (#14429)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
* test: cover agent skill lifecycles end to end * style: sort agent skill imports |
||
|
|
21dc4a2ef4
|
🎯 fix: Correct Off-by-One Rail Scrub After Pinning the Terminus (#14409)
Some checks failed
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Pinning the scroll-to-bottom rib moved it out of the column, but scrubTo kept enumerating ribs from the nav (messages + terminus) while measuring the fraction against the column, which now spans the messages alone. Every drag position mapped one rib late: pointing at the middle of the rail scrolled to the message below the rib under the cursor. Enumerate the column's own ribs for the proportional mapping and reach the terminus by dragging past the column's bottom edge, where it now sits. |
||
|
|
142973e7e8
|
🌍 i18n: Update translation.json with latest translations (#14406) | ||
|
|
60eba76375
|
🫙 fix: Preserve Loaded Message Content When Resume Snapshot Is Empty (#14399)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🛟 fix: Keep Loaded Message Content When a Resume Snapshot Is Empty The sync handler guarded on `data.resumeState?.aggregatedContent` and then assigned it unconditionally. An empty array is truthy, so a resume snapshot carrying no content overwrote the content the messages query had already loaded, leaving the message rendering as a bare cursor with no way to recover. Treat an empty snapshot as non-authoritative: keep the loaded content and let live deltas take over. A snapshot that carries content still wins, unchanged. This is defense in depth rather than a root cause. A resume snapshot goes empty when the conversation's job is replaced mid-flight (#14348) — because `streamId === conversationId`, a second submission calls `createJob` on the same key while the first run is still streaming. With this change that failure degrades to stale-but-visible instead of destroying content already on screen. * 🔒 fix: Scope Resume Content Preservation to Identity-Matched Responses Codex P2: on a regenerate reload before the new run aggregated anything, the server reports an empty snapshot under a response id not yet in the loaded history. The parent-based fallback then lands on the answer being REPLACED, and preserving its content seeded `syncStepMessage` with the stale response, so the regenerated run's deltas appended to it instead of starting blank. Preserve loaded content only when the row was matched by the server's declared `responseMessageId` — the sole case proving the row belongs to this generation. A fallback-matched row keeps the previous clear-on-empty behavior. * 🧱 fix: Only Preserve Resume Content When the Row Actually Has Parts A matched row with no `content` array would have been assigned `undefined` instead of the snapshot's array. `MultiMessage` branches on `message.content` truthiness to pick its renderer, and `[]` is truthy while `undefined` is not, so that would have silently switched a streaming row from the content-parts renderer to the text one. Preserve only when the loaded row has a non-empty content array — the case the guard exists for. Narrows the divergence from prior behavior further: it now applies solely when there is real content to protect. |
||
|
|
30ae414911
|
📌 fix: Pin Scroll-to-Bottom Rib in Message Nav (#14397)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 📌 fix: Pin Scroll-to-Bottom Rib in Message Nav Render the terminus rib outside the scrolling rail, between the column and the down chevron, so the scroll-to-bottom affordance stays in view no matter how far the rail has scrolled. - scrubTo enumerates ribs from the nav so drag-to-bottom still lands on the terminus - the pinned rib drives the shared preview itself on hover and focus, since it is no longer covered by the column's pointer magnification * 🖱️ fix: Keep Drag-Scrub Startable From the Pinned Terminus Pointer-down on the pinned rib no longer reaches the column's handler now that it renders outside the scrollport, so wire the same drag-start to the wrapper. Dragging up from the bottom dot scrubs the thread again. |
||
|
|
00c5a747e9
|
🧵 feat: Native Background Execution for Code Interpreter Tools (#14386)
* 🧵 feat: Native Background Execution for Code Interpreter Tools * 🩹 fix: Address Codex Round 1 (fallback dedupe, harvest failure, handle parsing) * 🩹 fix: Live Completion Marker + Unkeyed Attachment Dedupe (Codex Round 2) * 🎨 chore: Sort Imports + Widen Marker Type Comparison (CI) * 🩹 fix: Stale-Harvest Guard, Error Marker Status, Faster Anchor Retry (Codex Round 3) * 🧹 refactor: TS Harvest Module, Claim-Neutral Timestamps, Error Parity (Codex Round 4) * 🩹 fix: Dispatch-Ordered Stale Guard, Foreground Downgrade, Error Wrapper Parity (Codex Round 5) * 🩹 fix: Retry Past Unfinished Rows + Per-Call Attachment Dedupe (Codex Round 6) * 🩹 fix: Writer-Dispatch Ordering, Scoped Live Upserts, Reaped-Task Wrapper (Codex Round 7) * 🩹 fix: Wildcard toolCallId Matching for Bare Attachment Updates (CI) * 🩹 fix: Claim-Insert Dispatch Stamp (Schema-Backed) + Scoped Status Markers (Codex Round 8) * 🩹 fix: Pre-Write Ownership CAS + Agent-Scoped Part Patching (Codex Round 9) * 🩹 fix: Insert-Path Ownership CAS + Agent-Routed Attachments (Codex Round 10) * 🩹 fix: Agent-Scoped Marker Ids and Attachment Dedupe (Codex Round 11) * 🩹 fix: Atomic File Commit and Sibling Preview Fan-Out (Codex Round 12) - Replace the two-step claim-confirm CAS with an atomic conditional updateFile: the ownership predicate (no sourceDispatchedAt, or <= this write's dispatch order) moves into the update filter, removing confirmCodeFileOwnership and the lost-update window between check and write - Thread agentId through createDownloadFallback so fallback download rows scope to the emitting agent like primary rows - Fan terminal preview overlays out to every live attachment sharing the file_id in useAttachmentPreviewSync (sibling tool calls no longer stick on pending) - Restore background artifacts through toStoredArtifact so the size bound applies on re-anchor - Apply filterAttachmentsForPart to grouped tool-call attachments in ContentParts so handoff agents with colliding provider call ids do not cross-contaminate groups * 🩹 fix: Agent-Scoped Live Upserts and Monotonic Dispatch Stamps (Codex Round 13) - Scope the SSE attachment upsert and the useAttachments DB/live merge by agentId with the same wildcard semantics as toolCallId: distinct non-null agentIds stay separate entries, so handoff agents sharing a claimed file_id and a repeated provider tool id (call_0) no longer merge over each other's cards - Extend the attachment identity key to fileKey::toolCallId::agentId and register less-specific key variants so bare and agent-less live records still dedupe after overlay - Stamp background task createdAt from a strictly-increasing per-process dispatch counter: raw Date.now() can tie for same-millisecond dispatches and the stale-output guard accepts equal stamps (needed for idempotent re-commits), which would let an older task overwrite a newer task's committed file |
||
|
|
ad5bb477af
|
🎞️ fix: Surface Clear Error for Unprocessable Gemini YouTube Videos (#14396)
Google rejects a YouTube video it cannot ingest with a generic `400 INVALID_ARGUMENT` that names no cause, which LibreChat relayed verbatim. Attribute the failure using request context instead: when a Google/Vertex turn carried an injected YouTube video part and the provider returns that generic rejection, map it to a typed error the client localizes. Verified against the live API: a public 9h15m video is refused this way on gemini-2.5-flash, 3.5-flash, 3.5-flash-lite and 3.6-flash, including at MEDIA_RESOLUTION_LOW, while a short video with an identical payload succeeds. Duration is the dominant trigger; region and access restrictions return the same response, so the copy leads with length without overclaiming. A duration preflight was evaluated and skipped: oEmbed does not expose duration, leaving only watch-page scraping — a blocking call against undocumented markup from rate-limited datacenter IPs that would fail open and still need this mapping underneath. |
||
|
|
337facb4f0
|
✋ fix: Enable Submit on First Tool-Approval Decision (#14393) | ||
|
|
af7b2761eb
|
🪟 perf: Virtualize Search Results and Stop the Per-Query Remount (#14352)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🪟 perf: Virtualize Search Results and Stop the Per-Query Remount * 🔧 fix: Type-safe globalThis cast in Search route test * 🔑 fix: Stabilize Search Row Keys and Harden Virtualized Result Edges Address Codex review findings on the virtualized search results view: - Key rows by messageId (outer React key + CellMeasurer + cache keyMapper all aligned) so React reconciles by message, not scroll slot. - Compare title and conversationId in areSearchMessagePropsEqual so a rename or refetch that keeps text/id intact still re-renders the row. - Recompute cached heights on same-length content changes (file previews resolving, refetch) and on font-size changes. - Keep the aria-live announcement on the empty-results branch. - Don't paginate the outgoing query while the user is still typing. - Show the spinner during the initial debounce instead of a blank route. * 🧵 fix: Reset Scroll, Remeasure Growth, and Footer-Pad Virtualized Search Address the second Codex round on the virtualized search results view: - Reset the List scroll to the top on a new query (results stay mounted via keepPreviousData, so the List otherwise keeps the previous scrollTop and can open a new search mid-list); a font-size change keeps the user's place. - Re-measure a row when its content later grows/shrinks (tool/code output expands, a late image loads) via a ResizeObserver that clears just that row's cached height and recomputes from it. - Give the load-more throttle trailing:false and cancel it when the query changes, so a queued fetch can't page a stale search. - Add a fixed trailing spacer row so the last result clears the bottom gradient/spinner overlay. * 🫥 fix: Gate Stale Search Results on Refetch State, Not Just Typing Address the third Codex round: `isTyping` clears when the debounce publishes the new query, but `keepPreviousData` keeps the old pages mounted until the new request lands, leaving a window the typing-only guards missed. - Derive `showingStale = isTyping || isPreviousData` and gate both the dimming and pagination on it, so the outgoing results stay dimmed and don't page while the new query is still fetching. - Compare `unfinished` in areSearchMessagePropsEqual so a finish/cancel that changes only that flag re-renders SearchContent's incomplete-response notice. * 📐 fix: Invalidate Row Height Against the Cache and Compare clientTimestamp Address the fourth Codex round on virtualized search: - Compare each ResizeObserver height against the cached row height instead of skipping the first callback, so a cached/fast-loading image that is already taller than CellMeasurer's mount measurement still invalidates the stale height (no more overlap/clipping of following rows). - Compare clientTimestamp in areSearchMessagePropsEqual, since the row timestamp falls back to it when createdAt is absent. * 🕳️ fix: Spinner Over False Nothing-Found for Stale Empty Search Data Address the fifth Codex round: when the previous search had zero matches, keepPreviousData holds those empty pages (isPreviousData, isLoading false) during the new request, so the loading gate missed it and flashed a false "nothing found". Gate the spinner on `showingStale` too, not just isLoading/ isTyping. |
||
|
|
8751cc1c5c
|
🔗 fix: Preserve resource owner access when sharing (key share diff by stable id) (#14317)
* 🔗 fix: Preserve resource owner access when sharing (key share diff by stable id) The share dialog diff (GenericGrantAccessDialog.handleSave) keyed added/removed principals by `idOnTheSource`, which is inconsistent for the same user across sources: getResourcePermissions returns `userInfo.idOnTheSource || _id` (the external oid for OpenID/Entra users) while the people-picker returns the local `_id`. The resource owner then appears in both `updated` and `removed`, and — since updateResourcePermissions applies grants (upsert) before revocations (delete) — the owner's own ACL entry is deleted when they add anyone to the share list. They then get 403 on GET/edit/re-sharing their own resource. Extract the diff into a pure computeShareChanges() helper keyed by `id ?? idOnTheSource` (stable local id when present, external oid fallback for principals not yet synced locally, e.g. unsynced Entra groups/users). Add unit tests. Not reproducible with local-only users, where idOnTheSource falls back to _id and both sources agree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🔧 fix: address review — dedupe share diff by principalKey; drop test assertion - computeShareChanges now diffs over the de-duplicated map values, so a principal that appears more than once in the input (possible while the add/dedupe path still keys on idOnTheSource) is never emitted multiple times in updated/removed. - Drop the `as TPrincipal` assertion in the test helper — the literal is structurally compatible with TPrincipal, so TypeScript validates the shape directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cbaa2fe2e3
|
⚡ feat: Add Gemini 3.6 Flash and Gemini 3.5 Flash-Lite Support (#14369)
* ⚡ feat: Add Gemini 3.6 Flash and Gemini 3.5 Flash-Lite Support Adds first-class support for Google's Gemini 3.6 Flash (`gemini-3.6-flash`) and Gemini 3.5 Flash-Lite (`gemini-3.5-flash-lite`) for both the Gemini API (AI Studio) and Google Cloud/Vertex integrations. - Context window (1M) in googleModels; API + cache pricing in tx.ts. - Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations. - Generalize the Gemini 3.5 Flash overrides into a flash-family handler that strips deprecated temperature/topP/topK and applies each model's default thinking level (3.6 Flash: medium, 3.5 Flash-Lite: minimal), with longest-prefix resolution so flash-lite does not collide with flash. Ref: https://ai.google.dev/gemini-api/docs/latest-model#api-changes-and-parameter-updates * 🩹 fix: Strip unsupported penalty params for Gemini Flash family Gemini 3.6 Flash, 3.5 Flash-Lite, and 3.5 Flash reject presencePenalty/ frequencyPenalty with HTTP 400 ("Penalty is not enabled for this model", verified live). These pass through llmConfig via knownGoogleParams, so add them to the flash-family strip list alongside the deprecated sampling params. * 🩹 fix: Strip Flash-blocked params on custom Google endpoint path For custom OpenAI-compatible endpoints with defaultParamsEndpoint=google, getOpenAIConfig strips Flash-blocked params via getGoogleConfig but then transformToOpenAIConfig re-applies raw addParams, undoing the strip. Filter addParams through stripGeminiFlashBlockedParams before the transform so the deprecated sampling / rejected penalty params cannot reach the provider. * 🔧 chore: Update sharp package to version 0.35.3 in package-lock.json, api/package.json, and packages/api/package.json * 🔧 chore: Update dependencies in package-lock.json to latest versions for @google/genai (2.13.0), @hono/node-server (1.19.14), fast-uri (3.1.4), hono (4.12.31), and svgo (2.8.3) * 🔧 chore: Update dependencies in package.json and package-lock.json for @librechat/agents (3.2.67), @opentelemetry/sdk-node (0.221.0), and add new dependencies for @opentelemetry/propagator-jaeger (2.10.0) and protobufjs (7.6.5). Update monaco-editor version in client package.json to 0.56.0. * 🔧 chore: Upgrade turbo package to version 2.10.5 in package.json and package-lock.json, and update schema reference in turbo.json * 🩹 fix: Resolve CI breakage from bundled dependency bumps Not related to the Gemini models — both are fallout from the dep bumps on this branch: - monaco-editor 0.56 changed IEditorHoverOptions.enabled from boolean to 'on' | 'off' | 'onKeyboardModifier'; update ArtifactCodeEditor to match (mirrors the sibling occurrencesHighlight/matchBrackets pattern). - sharp 0.35.3 fails resize+encode on a degenerate 1x1 PNG (vipspng: libpng read error); the provider-file e2e fixture was 1x1, so use a 16x16 PNG. Normal images are unaffected (verified 64x64 resize/encode/jpeg all OK). * 📝 docs: Correct e2e image-fixture comment (bad IDAT CRC, not a sharp bug) Root cause was the old 1x1 fixture's corrupt IDAT CRC (verified: IHDR/IEND CRC OK, IDAT CRC BAD), which sharp 0.35.3's stricter libpng correctly rejects. Not a dimension/resize edge case and not a sharp bug; comment now reflects that. |
||
|
|
1dd7121d71
|
🌍 i18n: Update translation.json with latest translations (#14376) | ||
|
|
3f51fc5fbe
|
🧭 fix: Keep Message Nav Chevrons Working on In-Thread Steers (#14377)
An applied steer renders nested inside the response, whose `relative` content column becomes the steer's offsetParent, so its `offsetTop` is local to that column rather than measured against the scroll content like top-level rows. The rail compared that value against `scrollTop`, so once the viewport reached a steer every jump/current-row decision was computed in the wrong coordinate space — the "previous" chevron kept re-targeting the steer and got stuck. - Add `entryTop(el, container)` that sums `offsetTop` up the offsetParent chain until it leaves the scroll container, folding nested steers back into one content-space origin (top-level rows collapse to a single hop) - Use it for the four message-entry measurements (current row, offset cache, jump previous/next); leave the column-rib offsets untouched - Guard `getCurrentVisibleId` on a null scroll container - Test a nested steer (local offset inside a positioned column) lands its rib at the true thread position; fails with the old single-hop offsetTop |
||
|
|
ade02054c8
|
🛟 fix: Keep File Uploads Alive With SSE Heartbeats (#14295)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: Use SSE to upload files in order to avoid idle timeouts. Idle timeouts can occur for example from gateways and other services like cloudfare when uploading large files. For example during rag processing the file is uploaded to librechat which then sends it to rag. While librechat is waiting for the embeddings to come back from rag the file upload is sitting idle. Gateways tend to want to cancel the upload with an http 408 , 504, or 524. This change uses SSE to perform the upload so that while librechat is sending the file to rag, it consistently sends back a heartbeat event to the client to keep the connection alive. This is especially useful when utilizing EMBEDDING_BATCH_SIZE in librechat rag which will allow rag to process signifigantly larger files without running out of memory. * added tests to packages\api\src\files\sse.spec.ts in order to test the new sse.ts * fix: Harden SSE file upload lifecycle * style: Sort data provider imports --------- Co-authored-by: Marc Amick <MarcAmick@jhu.edu> Co-authored-by: Danny Avila <danny@librechat.ai> |
||
|
|
7406f5d79e
|
🌍 i18n: Update translation.json with latest translations (#14307) | ||
|
|
21766b5b3c
|
🧠 fix: Restore Agent Memory Scope Control in Unified Builder (#14292)
* 🧠 fix: Restore Agent Memory Scope Control in Unified Builder The Agent Builder redesign (#13952) surfaced memory as a Tools marketplace item and stopped rendering SidePanel/Agents/Memory.tsx, orphaning the file and removing the 'Keep memories separate for this agent' control shipped in #14084. Only the render was lost: the locale keys, memory_scope on AgentForm, the AgentPanel save path, and AgentSelect hydration all survived, which is why setting memory_scope directly on the agent document still worked. Restore it as a builtin item setting (the seam the new builder uses for per-tool config): mark memory configurable so its row gets a cog, and render a MemoryConfig branch in BuiltinSection mirroring ArtifactsConfig. Delete the orphaned component so there is a single source of truth. Resolves #14287 * 🧹 chore: Remove Orphaned com_agents_enable_memory i18n Key The key labeled the enable checkbox in the deleted SidePanel/Agents/Memory.tsx. The unified builder labels the catalog item via com_ui_memory, so it has no remaining consumer and detect-unused-i18n-keys fails on it. |
||
|
|
87b3557f11
|
🫧 feat: Float In-Flight Steers Over the Thread with Collapsible Content (#14365)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
- Float the in-flight steer stack as a bottom-anchored overlay (`absolute bottom-full`) so it no longer shrinks the message viewport and older messages scroll behind it; a per-conversation Jotai height atom reserves an equal band of bottom padding on the message scroll area so the newest message rests clear - Collapse long steers to a ~128px preview with a bottom fade and a Show more/Show less toggle, shown only once the content overflows the cap - Sticky the per-steer options menu (`sticky top-2`) so it stays reachable while scrolling through a tall, expanded steer |
||
|
|
ad46f66dc4
|
🧬 perf: Memoize Message Spine and Isolate Scroll-Button State (#14330)
* 🧬 perf: Memoize Message Spine and Isolate Scroll-Button State * ✅ test: Pin the message-row memo comparators against field drift areMessageFieldsEqual and areMessageRowPropsEqual gate every message row's re-render but had no direct tests. Add a completeness suite: a field-mutation table asserts each compared field flips the comparator to false (a dropped field fails its case), plus same-ref / equal-distinct-objects / nullish cases, and the same shape for the row-props comparator including its delegation into areMessageFieldsEqual. |
||
|
|
71fa24a6ea
|
🎛️ perf: Narrow Composer Subscriptions to Streaming State (#14333)
* 🎛️ perf: Narrow Composer Subscriptions to Streaming State * ✅ test: Cover Composer Subscription Refactor's Behavioral Changes Add regression tests for the previously-uncovered changed behavior in the composer-subscriptions refactor: - useLatestMessageMeta: exact projected field set, null on empty cache, and referential stability + no re-render across token-only cache writes. - useGetLatestMessage: call-time tail read, stable callback identity with no re-render on cache writes, Recoil-snapshot sibling-branch resolution, null with no conversation. - useSubmitMessage: reads the tail at call time and appends it to root when missing (and does not when present or absent) — the reconcile branch the prior test skipped via an early return. - useHandleKeyUp: ArrowUp in an empty composer clicks the latest message's edit control, with the null / missing-control / non-empty-composer guards. - useAskAnswerMode (new spec): liveAsk is projected through the findLiveAskUserQuestion select, null when empty/disabled. * 🎨 style: Fix import order in useLatestMessage spec |
||
|
|
9e245aced4
|
🎟️ fix: Claim Idempotency Keys to Dedup Retried Generation Requests and Prevent Double Billing (#14344)
* 🐛 fix: Dedup retried start-generation requests to prevent duplicate billing A lost or reset start-generation response makes the client re-POST the identical payload (up to 3x on network errors). The resumable-stream controller had no idempotency: createJob unconditionally overwrote the running job without aborting the prior one, so both requests ran full LLM completions and both billed while the UI showed only one (#14339). Add a stable per-submission clientRequestId (uuid, fresh per ask() so a regenerate differs, reused across the start-generation retries) and an atomic claim on the job store keyed by userId:clientRequestId. The first request wins and generates; a retried POST loses the claim and receives the original stream, which the client subscribes to and replays - no second billed generation. - IJobStore.claimIdempotencyKey/releaseIdempotencyKey (in-memory Map+TTL, Redis single-key SET NX PX + GET Lua, cluster-safe) - GenerationJobManager.claimGeneration/releaseGeneration (20m TTL) - Controller claims before the concurrency check, dedups with a resumed response, releases on start-failure/429 - clientRequestId threaded through TSubmission/TPayload/createPayload * 🐛 fix: Harden start-generation dedup (Codex review) Address three P2 findings on the idempotency path: - Resume replay: a deduped retry now subscribes with resume=true so the client replays prior content and any pending-action from the running stream instead of only live events (cross-replica / HITL correctness). startGeneration returns { streamId, resumed } and the response's status:'resumed' drives the subscribe mode. - Wait for the job record: a duplicate that loses the claim now waits briefly for the winner to create the job before returning the stream (a stream with no job 404s terminally). If the winner has not materialized, return 503 SERVER_NOT_READY so the client retries via the existing readiness path instead of attaching to a dead stream. - Release only owned claims: track whether the request actually won the claim; the 429 and init-error paths no longer release a claim owned by another in-flight generation (fail-open path could erase it and re-enable double billing). Adds controller tests covering dedup, the 503 race fallback, win-then- create, and claim-release ownership on 429 / fail-open. * 🐛 fix: Don't trap deduped retries on missing job records (Codex review) The previous round returned 503 SERVER_NOT_READY when a deduped retry's job record was absent. But a missing job usually means the original generation already completed and was cleaned up (cleanupOnComplete) — the correct recovery is to return the stream and let the client's subscribe 404 handler refetch the persisted messages. The 503 instead trapped the send in a readiness-retry loop until the client's window expired. Keep the bounded wait (it still covers the job-about-to-be-created race) but always return the resumed stream afterward; a gone/never-created job recovers via the client's existing 404 path instead of being treated as indefinitely starting. Updated the controller test accordingly. * 🐛 fix: Gate deduped resume on claim age, not just job presence (Codex review) Removing the 503 entirely (previous round) reintroduced the inverse race: if the winning request stalls between claimGeneration and createJob, a losing duplicate saw no job, returned status:'resumed' anyway, and the client subscribed to a stream that did not exist yet — the 404 handler tore the turn down while the winner went on to generate and bill with no UI attached. Distinguish the two missing-job cases by claim age (claimedAt now travels on the claim value): - fresh claim, no job yet → winner is still starting → 503 SERVER_NOT_READY so the client retries via the readiness path (bounded, not indefinite). - old claim, no job → the original already completed and was cleaned up (or the winner died) → attach; the client's 404 handler refetches. Tests cover both age branches. * 🐛 fix: Scope dedup fail-open + keep resumed convos on 404 (Codex review) - Fail-open only on claim acquisition: a store error while checking an already-confirmed existing claim no longer falls through to createJob (which would start a second billed generation during a Redis hiccup). Once claim.existing is known, a job-lookup error returns 503 retry. - Don't drop a resumed convo on 404: the optimistic-conversation cleanup in useResumableSSE now runs only for fresh (non-resume) subscribes. A deduped resume whose original completed and was cleaned up 404s, but its conversation is persisted and must stay in the sidebar. Adds a controller test for the job-lookup-error path (503, no createJob). * 🐛 fix: Reconcile resumed convos on 404 instead of guessing (Codex review) Round-4's !isResume guard fixed the completed-and-cleaned case (don't drop a persisted convo) but left the inverse: a new-conversation retry deduped to a claim whose original worker died before persisting still resumes, 404s, and — with removal skipped — leaves a phantom /c/<streamId> sidebar entry. Stop guessing keep-vs-remove on a resume 404. Reconcile against the server: invalidate the conversations list so a real (persisted) convo stays and a phantom is dropped. Fresh (non-resume) optimistic streams still prune immediately. Adds a client test for the resume path. * 🐛 fix: Finalize failed job before releasing its claim (Codex review) In the initialization-error catch, the idempotency claim was released before completeJob(streamId). A racing retry could win the released key and createJob() the same streamId while this catch was still running, and completeJob() (not guarded by the original createdAt) would then abort the replacement. Finalize the failed job first, then release the claim. Adds a controller test asserting completeJob precedes releaseGeneration. * 🐛 fix: Clear claims on destroy + survive completeJob failure (Codex review) - InMemoryJobStore.destroy() now clears the idempotencyClaims map, so a reused/reconfigured store instance doesn't dedup a fresh start against a torn-down job's stale claim. - Init-error cleanup: completeJob() is swallowed so a store-hiccup rejection can no longer skip the idempotency-key release and the pending-request decrement (which would wedge the retry behind the claim and leak the concurrency slot). A failed completeJob finalized nothing, so releasing afterward still can't abort a later replacement. Tests: claims cleared on destroy; release + pending decrement still run when completeJob rejects. |
||
|
|
3171b86413
|
🎞️ perf: Coalesce Streaming Delta Cache Writes Per Animation Frame (#14332)
* 🎞️ perf: Coalesce Streaming Delta Cache Writes Per Animation Frame * 🎞️ fix: Cancel Pending Delta Flush Before Standard-Path Terminal Writes * 🎞️ fix: Flush Queued Deltas at Abort, Error, and Pending-Action Boundaries |
||
|
|
f4a0e0c194
|
🧹 perf: Share Voices Store, Gate Timestamp Ticker, Stabilize Greeting Springs (#14335) | ||
|
|
eeb4ea226c
|
🧭 perf: Warm Conversation Switches with Single-Navigation Focus Intent (#14334)
* 🧭 perf: Warm Conversation Switches with Single-Navigation Focus Intent * 🧭 fix: Drop Warm Message Cache When Conversation Revalidation Fails * 🧭 fix: Defer Departing-Convo Refetch and Gate Resume on Revalidation * 🧭 fix: Gate Stale-Cache Sends During Revalidation and Honor disableFocus |
||
|
|
1a58c72444
|
🧩 chore: Prebundle Node Polyfills for Buffer, Process, and Global in Vite optimizeDeps (#14354)
Enhance the Vite configuration by including specific node polyfills in the `optimizeDeps` section. This change ensures that the necessary polyfills for `buffer`, `process`, and `global` are optimized for better performance during development. This adjustment aims to improve compatibility and streamline the build process. |
||
|
|
d5e8c5c15e
|
🚰 perf: Suppress No-Op Conversation Writes and Widen-Proof Atom Subscriptions (#14329) | ||
|
|
a8ecdd6226
|
🧷 perf: Stabilize Mutation-Dependent Memos in Chat Hooks (#14328) | ||
|
|
56ecb6494c
|
✋ feat: Add Reclaim-Gated Controls to In-Flight Steers (#14321)
* ✋ feat: Add Reclaim-Gated Controls to In-Flight Steers Give a pending steer the same controls as a queued message — edit, convert to queue, and the during-run mode toggle — instead of only a bare cancel. Every re-homing action reclaims the steer from the server queue first and acts only on a confirmed `removed: true`. A steer leaves that queue only by injecting, so a lost race means the words are already in the run: queueing or editing them then would say the same thing twice. - Return a `SteerCancelOutcome` ('reclaimed' | 'applied' | 'failed') from useSteerCancel so callers can distinguish "the words are still mine" from "already injected" and "unknown fate" — the last two only toast. - Extract RowMenu, useDefaultToggleEntry, and the shared button classes into SteerMenu so both during-run surfaces use one implementation. - Offer controls only once `pending`: a `sending` steer has no server id to reclaim with, so its words cannot be held back. - Pin the control cluster visible while its menu is open — the portaled items hold focus outside the subtree, so `focus-within` alone would drop it. * 🩹 fix: Address Codex Findings on Reclaimed-Steer Controls Route a reclaimed steer through the shared conversion, and stop the async reclaim from stranding items or clobbering a composer that moved on. - Queue a reclaimed steer via useSteerConvert instead of enqueue, so it keeps its original id and createdAt. enqueue minted a fresh v4/Date.now() and appended, so a steer accepted BEFORE a later follow-up drained after it — breaking the invariant the leftover-steer path documents. - Submit the item directly when the run ended during the reclaim round-trip: the drain consumes its one-shot signal against an empty queue, so nothing was left to auto-send it. Read run state and conversation from refs, since the reclaim resolves after the bubble unmounts. - Refuse the composer restore when the origin conversation no longer matches or a newer draft is present, and queue the words instead of overwriting them. Neither text is the one to throw away. - Split useSteerReclaim (POST only) out of useSteerCancel, so the menu actions leave the chip alone until the outcome is known while the X stays optimistic. * 🛡️ fix: Harden Reclaimed-Steer Guards Against Stale State Both guards from the previous round read values that had moved on by the time the reclaim resolved. - Compare the origin conversation against a ref, not the closure. The `.then` holds `restoreReclaimedSteer` from the render it was clicked in, so its captured `conversationId` is the OLD chat — the guard compared that against itself and passed, while `methods` (one form, reused across conversations) wrote the steer into the chat now on screen. - Gate the direct send on the drain's own rule. `!isSubmitting` also covers a Stop or an error, so converting and then pressing Stop auto-sent the text past useQueueDrain's completed-or-armed-interrupt-only rule. Capture the run's outcome before the drain consumes the one-shot signal, and send only on a clean completion of THIS conversation. * ♻️ refactor: Re-Arm the Drain Instead of Direct-Sending Reclaimed Steers The direct send was the wrong mechanism: it re-implemented the drain badly, and each round of review found another rule it had skipped. Delete it and let useQueueDrain do the sending — it already owns every one of those rules. - Re-post the spent run-end signal under the conversation instead of calling sendNow. The drain then applies the completed-only rule, FIFO order (an older follow-up is no longer skipped), NEW_CONVO migration, and submits via `ask` — which, unlike the composer's sendNow, does not reset the form and so cannot wipe a draft typed while the reclaim was in flight. - No-op when a signal is still armed: that drain has not run yet and will see the item on its own, so arming a second carrier would send twice. - Watch the parked run-end too, not just the index one, so a run that ended while the user was in another chat is still seen. - Treat staged files, quotes, and skill picks as a draft when deciding whether a restore may overwrite the composer — editToComposer MERGES into them, so restoring over staged context would glue two submissions together. * 🎯 fix: Scope the Re-Arm Suppression to This Conversation The no-double-arm guard treated ANY armed index run-end as proof the drain would see this conversation's newly queued item. The index slot is shared: useQueueDrain parks a foreign signal under its own conversation and then inspects only the active one's queue, so a reclaimed steer sitting behind an unrelated run-end would never be looked at and would strand until sent by hand. Suppress only when the armed index signal belongs to THIS conversation — which is the case where the drain really will see the item. The parked check was already conversation-scoped by its key. * 🧭 fix: Trust the Refs Only While They Describe This Chat useSteering is reused across conversations, so after a navigation its live refs describe the NEW chat while the reclaim's callback still speaks for the old one. Restoring the conversation-identity guard I removed last round, which was wrong precisely because the refs are live but not conversation-scoped. - Skip the re-arm entirely once conversationIdRef no longer matches the steer's conversation. Reading isSubmittingRef there could suppress a needed re-arm, and lastRunEndRef could hold the NEW chat's run-end — parking that under the old conversation would make drainNext (which keys off end.conversationId) drain the wrong queue into the wrong chat. - Assert lastRunEnd.conversationId matches before re-arming, so the invariant is enforced where it is relied on rather than inferred from render order. Nothing is lost by stopping: the item is already queued under its own conversation, and that run's end parks under it and drains on return. * 🗝️ fix: Key the Captured Run-End by Conversation A single run-end slot could only answer for whichever chat was on screen when a reclaim landed, so the guard had to bail on navigation — stranding a steer whose run had already completed, contrary to "queue for after the response". Key the captured run-ends by conversation instead. The stored end always speaks for the chat the words belong to, so navigating away no longer suppresses the re-arm, and another chat's end can never be parked under this one (which would hand drainNext a foreign end.conversationId and drain the wrong queue). - Drop an entry when its conversation starts another run: a superseded end must not authorize a drain of the run now in flight. This replaces the isSubmitting guard, which described the wrong chat after navigation. - Remove conversationIdRef, now that no read depends on where the user is. * 🧹 fix: Close Three Reclaim Races Around Answer Mode and Run End - Refuse the composer restore while answer mode is active. `onSubmit` hands composer text to `answerMode.submitText` before any send/steer routing, so a restored steer would become the tool's answer on the next Enter. Read through a ref: the run can pause on ask_user_question mid-reclaim. - Skip the restore when a terminal conversion already queued the words. The chip stays interactive during the reclaim round-trip, so a run ending or erroring meanwhile converts it — restoring after would leave one copy queued and another in the draft. The queue action needed no guard; the conversion already dedupes by id. - Carry quotes/skill picks on the reclaimed steer itself. The conversion recovers them from the chip, which a competing X can delete mid-round-trip, silently dropping the picks. * 🎛️ feat: Fold Cancel Into an Always-Visible Steer Menu Make the in-flight steer bubble a single, discoverable affordance instead of two hover-hidden ones, matching how Codex/ChatGPT present the same control. - Fold Cancel into the ⋯ menu as an item (X icon), removing the standalone X button. It keeps the optimistic `useSteerCancel` path — no reclaim gate, since cancel drops the words rather than re-homing them. - Show the ⋯ at rest on every pointer instead of hover-gating it. A label-less menu hidden until hover is undiscoverable on desktop and unreachable on touch; always-visible also matches the queued rows' controls and drops the hover/focus/menu-open opacity juggling entirely. * 🪢 feat: Make Cancel and Queued Trash Non-Destructive Both removal actions now hand their text back to the composer instead of dropping it, so a message the user typed is never gone forever. - In-flight Cancel: before cancelling, restore the words to the composer via the gated `restoreReclaimedSteer` (skipped once applied — they are already in the response). The restore refuses on its own rather than clobber a draft, land in another chat, or fight answer mode; the cancel still runs reliably either way, so an unwanted steer stays killable. - Queued Trash: same safety net — thread the gated restore into the queued rows and return the words (with their carried quotes/skills) to the composer, then remove either way. Aligns the two surfaces on one behavior. - Export the shared `RestoreToComposer` type so both surfaces reuse it. Left the reliable-remove path intact (a steer sometimes must be killed before it reaches the model) and did NOT reach for a delete+Undo snackbar, which the shared Toast can't render without a cross-cutting action-button change. * 🔒 fix: Restore Cancelled Steer Text Only on a Reclaimed Outcome The cancel safety net restored the words to the composer synchronously, before the cancel POST resolved. On `applied` (cancel lost the race, steer still injects) or `failed` (POST errored, chip restored), the same text ended up both in the run/bubble and in the composer. Await `cancelSteer`'s outcome and restore ONLY on `reclaimed` — the one result that proves the steer never reached the run. `applied`/`failed` leave the words where the events place them, no composer copy. The gated restore still refuses rather than clobber a draft typed during the round-trip. * 🧵 fix: Never Drop Cancelled Text; Keep the Steer's Submit Time Two follow-ups on the cancel safety net. - Cancel no longer silently drops the words when the reclaim succeeds but the composer refuses the restore (draft typed, answer mode, navigated). The chip is already gone, so queue them like Edit does — never lost, just re-homed — with the same toast. - Preserve the true submission timestamp across submitSteer's chip states. The ACK and failure chips reset createdAt to a LATER Date.now(), so a draft queued during the 202 round-trip could sort ahead of a steer submitted before it and drain out of order. Capture the submit time once and reuse it for all three. * 🚪 fix: Refuse Reclaimed-Steer Restore Into an Unmounted Composer A reclaim/cancel round-trip can resolve after ChatForm unmounts (left the route, closed the pane). Its refs still hold the origin conversation, so `restoreReclaimedSteer` passed its checks, wrote into a dead form, and returned true — making the caller drop the steer instead of queueing it, losing the text. Track mount state and refuse the restore once unmounted, so the caller queues the words (recoil is global, so the queued chip survives the navigation). |
||
|
|
b04ff2648e
|
📱 fix: Don't Connect the Favorites Drag Source on Touch Pointers (#14312)
#14272 gated the hover-revealed "..." button on hover capability, but pinned agents still take two taps on iOS. That fix was aimed at the wrong mechanism for this list. Every favorite row is wrapped by DraggableFavoriteItem, and react-dnd's HTML5Backend stamps `draggable="true"` on that wrapper unconditionally (connectDragSource, HTML5BackendImpl.js:101 — `canDrag: false` does not suppress it, react-dnd#2909). iOS Safari hands a touch on a draggable element to the drag recognizer rather than synthesizing a click, so the row underneath only selects on the second tap. The draggable wrapper is what separates favorites from every other sidebar row. Conversation rows are more hover-dependent than favorites ever were (ungated `opacity-0 group-hover:opacity-100` plus an onMouseEnter that mounts ConvoOptions) and select on the first tap. Connect the drag source only under `(hover: hover)`. Nothing is lost on touch: HTML5Backend has no touch support, so drag-to-reorder never worked there. Passing null to the connector unsubscribes cleanly and resets the attribute, so a hybrid pointer flipping the query re-arms drag. |
||
|
|
bd1df30b7d
|
🔒 fix: Scope, Cap, and De-Execute the In-Flight Steer Stack (#14310)
* 🔒 fix: Scope, Cap, and De-Execute the In-Flight Steer Stack Codex review on |
||
|
|
8f712259ea
|
💬 refactor: Anchor In-Flight Steers Above the Composer (#14308)
* 💬 refactor: Anchor In-Flight Steers Above the Composer Mid-run steers were rendered in-thread at the tail of the streaming assistant message, at a guessed injection point, then swapped to the persisted STEER part at its real index once the server applied them. In-flight steers now render as message bubbles anchored above the composer, so the thread only ever shows what the server committed: - InFlightSteers: sending/pending steers as left-aligned bubbles with image previews and a cancel affordance, anchored above the composer box - PendingSteerChips: unchanged, still owns the failed/queued control rows - SteerPart: drops the pending/onCancel props, now only ever the server-applied part - useSteerCancel: the optimistic cancel + restore-on-error, lifted out of the deleted PendingSteers slot The steer state machine is untouched: the 202 ACK reconciliation, reconnect reseeding, and queue conversion all key off status, not render location. * 🎨 fix: Match In-Flight Steer Presentation to the Applied Part Codex review on 6a5f36f7ef. All three findings were real, and all three were the same underlying mistake: the anchored bubble hand-rolled presentation instead of reusing the leaves the applied SteerPart uses, so a steer visibly changed on apply. - Images: the message `Image` sets an inline height from the file's dimensions and centers with object-contain, so clipping it into a 56px wrapper showed the blank top of a large element. Use ImagePreview, the composer's fixed-size thumbnail path (also gives click-to-enlarge). - Non-image files: FileContainer always renders a button, so without an onClick the chip was dead. Wire FilePreviewDialog, as SteerPart does. - Markdown: honor enableUserMsgMarkdown so text does not reflow the moment the server injects it. Splits files in a single pass rather than two filters. * 🎨 style: Outline the In-Flight Steer Bubble and Move the Bolt Inline The filled bubble read as a settled message. An outline reads as provisional, which is what an in-flight steer is, and separates it from the composer surface behind it. - Border + bubble keeps the composer's rounded-3xl radius so it reads as anchored to the input rather than floating over it. Border stays NEUTRAL: the failed-steer row already owns a colored (red) border, so a colored outline on the happy path would read as a warning. - The Zap moves inside the bubble, left of the text, where it prefixes the words as a status label instead of competing with cancel for the right edge. items-start pins it to the first line when text wraps. - Cancel drops plain `opacity-0` for `[@media(hover:hover)]:opacity-0`, matching SteerPart's info affordance: a hover-revealed control is unreachable on touch until a first tap (the #14272 pattern). |
||
|
|
035228360d
|
🙋 fix: Stop answered ask_user_question card from reopening its popover (#14297) | ||
|
|
9c7547db96
|
🧷 fix: Flush Pending File Deletion on Unmount (#14293)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Removing the last file from an agent's Context panel fired no request and the file reappeared on reload. `FileContext` mounts `FileRow` only while `fileCount > 0`, and `useFileDeletion` lives inside `FileRow` behind a 1s debounce whose unmount cleanup called `debouncedDelete.cancel()`. Deleting the last file drops the count to 0, unmounting `FileRow` and cancelling the delete the user had already confirmed. Removing a non-last file kept the row mounted and worked, which made the failure look erratic. The same cancel also dropped deletes in any panel closed within the debounce window. Flush the pending batch on unmount instead of cancelling it. |
||
|
|
f1b9c5f091
|
🍽️ chore: Drop Pending Composer Draft When Steering or Queuing (#14289)
* 🧹 fix: Drop Pending Composer Draft When Steering or Queuing A during-run submit takes the composer text into a steer or a queued item and clears the composer via the form's `reset()`. That clear is programmatic, so it never fires the `input` event `useAutoSave` listens on, leaving the autosaved draft (keyed under `PENDING_CONVO` for the duration of the run) behind. When the run ends, `useAutoSave` migrates a surviving pending draft onto the real conversation id and restores it into the textarea. The result: a queued message that was successfully auto-sent by the run-end drain immediately resurfaced as the composer draft, and persisted there under the conversation key across reloads. Consume the pending draft at the three composer-origin entry points (steer, queue, interrupt & send), mirroring the existing takeComposerFiles/takeComposerContext consumption helpers. Only a consumed submit clears it — a refused one (empty text, uploads in flight) leaves the draft intact. * 🔒 fix: Flush The Live Composer Value On Debounced Autosave Codex round 1: the 25ms debounced autosave captured the textarea value at event time, so a write scheduled just before a during-run steer/queue could land after the composer was consumed and cleared — rewriting the just-sent text back into the PENDING_CONVO draft and defeating the clear. Read the value at flush time instead. When the composer was cleared in the debounce window the pending write now removes the draft rather than resurrecting it, and an untouched composer saves exactly as before. |
||
|
|
eccc7d58e9
|
🧟 fix: Prevent Drained Steer From Re-Queuing After Run-End Race (#14276) | ||
|
|
7447fddfb2
|
🙊 refactor: Clarify Ask Question Schema Errors and Retry Guidance (#14279)
* fix(agents): clarify ask question validation errors * fix(agents): narrow question failure detection * fix(agents): persist question validation failures * fix(agents): track question validation failures |
||
|
|
b7542871b9
|
🌍 i18n: Update translation.json with latest translations (#14266)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
|
||
|
|
305e0f5003
|
🧽 fix: Clear Deleted Chats From Message Cache (#14270)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix: clear deleted conversation message caches * test: cover deleted chat cache cleanup * test: clarify deleted cache scenarios |
||
|
|
c865de99a5
|
🛞 style: Reveal the Steered "?" on Message Hover/Focus (#14271)
* 👀 fix: Reveal the Steered "?" on Message Hover/Focus The steered-message "?" InfoHoverCard sat on every steered message at rest. Wrap it like the message hover buttons so it stays transparent until the message is hovered (group-hover) or the trigger is focused (focus-within), keeping the thread clean. * 📱 fix: Keep the Steered "?" Visible on Touch (Codex) Plain opacity-0 hid the info affordance on touch entirely, with no hover path to reveal it. Gate the hidden-at-rest state on hover capability ([@media(hover:hover)]:opacity-0), matching the message hover controls: visible on touch, revealed on hover/focus on hover-capable pointers. |
||
|
|
d922e1ef79
|
📱 fix: Single-Tap Select for Pinned Agents, Model Specs & Models on Touch (#14272)
The pin/options buttons on these items were hidden-until-hover on ALL pointers (invisible / opacity-0), making each item's rendering hover-dependent. On touch that triggers the iOS "first tap reveals the hover state, second tap activates" behavior, so selecting a model spec, model, or pinned/favorite agent took two taps. Gate the hover-reveal on hover CAPABILITY (the same fix #13712 applied to message hover buttons): the control is visible/tappable by default and only hidden-until-hover on hover-capable pointers via [@media(hover:hover)]. On touch the item is no longer hover-dependent, so the first tap selects. - ModelSpecItem / EndpointModelItem: pin button reveal - FavoriteItem (sidebar pinned agents): options button + wrapper reveal |
||
|
|
5b0330fdfb
|
💬 feat: Explain Steering & Queuing at Every User-Facing Surface (#14260)
* 💬 feat: Explain Steering & Queuing at Every User-Facing Surface Adds localized info affordances so users understand what steer / queue / interrupt do wherever the feature appears, using the app's existing norms (InfoHoverCard in Settings, TooltipAnchor in the composer, inline descriptions in menus). - Settings → Chat: an InfoHoverCard "?" beside the during-run action toggle explaining steer vs queue (matches every neighboring setting) - During-run send button: the hover action list gains a header and a one-line description under each action (steer / queue / interrupt), turning the menu into a self-explaining card - "Turn on steering / queueing" overflow entry: a description sub-line explaining the mode the user would switch to (the label alone doesn't say what steering/queueing means) - Steered in-thread message: a subtle "Steered" badge with a tooltip clarifying why a user message appears inside the assistant response - New English keys only (others automated) * 🔤 style: Sort SteerPart imports (repo import-order) * 🎐 refactor: Subtle "?" Info Affordances for Steering (Feedback) Reworked the info UI toward the app's "?" InfoHoverCard norm and away from always-on text / a loud badge: - Steered message: replaced the amber "Steered" pill with a subtle "?" InfoHoverCard in the header (the norm; muted, hover-reveals why a user message appears inside the response) - During-run send button: reverted the per-action description lines — the hovercard is already a hover-reveal affordance, so it stays a clean action list (labels + shortcuts) - "Turn on steering/queueing" overflow entry: reverted the description sub-line back to a clean menu item - Settings → Chat "While generating, Enter will…": keeps its "?" InfoHoverCard (the canonical, discoverable explanation) - Pruned the 5 now-unused i18n keys; kept com_nav_info_during_run_action and com_ui_steered_info |
||
|
|
39a32561b2
|
🤝 fix: Discover MCP OAuth Exchange Methods (#14256)
* fix: discover MCP OAuth exchange methods * fix: bound configured OAuth discovery * fix: preserve configured OAuth resource discovery * test: model absent OAuth resource metadata |
||
|
|
5771bf6e06
|
♨️ feat: Prewarm Stateful Code Sandboxes with Cold-Boot UX Feedback (#14239)
* ♨️ feat: Prewarm Stateful Code Sandboxes with Cold-Boot UX Feedback * 🧹 fix: Drain Prewarm Response + Reset Sandbox Atoms on Stream Cleanup * 🚿 fix: Propagate Prewarm Drain Failures + Warm Marker for Host File Tools * 🌡️ fix: Decouple Prewarm In-Flight State from Warm Refreshes + Precise Ready Gates * ☁️ refactor: Redis-Backed Sandbox Prewarm State via standardCache * 🧪 chore: Hermetic Prewarm Spec + Accurate Signal JSDoc (Copilot review) |
||
|
|
9bb351ad9c
|
🧭 feat: Mid-Run Steering and Queued Messages for Agent Runs (#14220)
* 🧭 feat: Mid-Run Steering and Queued Messages for Agent Runs Steering: submit a message while a run is generating; the server queues it in the job store (cross-instance) and a run-scoped PostToolBatch hook injects it into graph state at the next tool-batch boundary, records an inline 'steer' content part on the response (replayed as a user message on later turns), and streams on_steer_applied to the client. Queuing: messages composed during a run auto-send as normal follow-up turns after clean completion (one per final event, FIFO); user aborts leave them as chips unless armed by interrupt-and-send. Requires hook injectedMessages support in @librechat/agents (danny-avila/agents#299); hard-gated via a capability probe so older SDKs 501 the steer route instead of draining and dropping messages. * 🧵 fix: Harden Steering Against Finalization Races and Route Guard Gaps Addresses local Codex review findings on the steering feature: - Close-and-drain the steer queue atomically at finalization (final event, abort) so a steer POST racing teardown is rejected instead of 202-ACKed and then silently cleared; the closed flag lives on the job hash and is reset when a replacement job reuses the stream id. - Clear inherited steer queues on createJob — a job replacement must not drain the replaced run's messages. - Keep steers queued across a HITL pause instead of draining them into ephemeral client state: resumeState re-seeds chips on reload and the resumed run injects them at its first tool boundary (steers key TTL now extends to the approval window; on_steers_pending event removed). - Queue the NO_ACTIVE_RUN steer fallback while the final SSE is still settling — a direct send would be dropped by ask()'s in-flight guard. - Reconcile the 202 ACK against on_steer_applied events that beat it over the SSE, so a chip can't be re-minted after its removal event passed. - Allow the per-send Steer override when the default action is queue. - Apply the configured message rate limiters and the PII filter to POST /chat/steer — a steer is model-bound user text. * ✅ ci: Assert Steering Capability Probe Against the Installed SDK CI installs the published @librechat/agents pin (pre-injectedMessages), where isSteeringSupported() is legitimately false — the probe test now asserts it mirrors the installed SDK's capability flag instead of hardcoding the capability-bearing build's value. Verified against both the published 3.2.61 dist and the agents#299 build. * 🛟 fix: Preserve Steer Text Across Run-End, Error, and Abort Races Codex round 2 (4 P2s): - Applied-steer-id set survives run end (capped at 100) and converted ids join it, so a 202 ACK that lands after final/abort drops its chip instead of re-minting a stranded pending one. - Failed runs no longer strand acknowledged chips: both error paths convert local pending chips to queued follow-ups (chip text is client-local), and the server closes the steer queue before emitting the error so a racing steer POST gets 404 fallback instead of a 202 whose payload dies with the job. - sendQueuedNow keys on steer availability, not the default action — send-now on a queued chip is an explicit override for queue-preferring users. - Stop path consumes pendingSteers from the abort HTTP response as a fallback for the SSE final event it may close before processing; conversion is deduped so double delivery is a no-op (shared useSteerConvert hook). * 📎 feat: Carry Attachments Through During-Run Queued Messages Steering stays text-only (SDK injection, inline STEER part, and replay are all text), so a during-run submit with media now queues the whole message as one unit instead of silently stranding the files: - QueuedMessage gains `files`; composer attachments are consumed into the queued item at queue time (steerFromComposer / queueFromComposer / interruptAndSend), fixing the latent hazard where lingering composer files glued onto whatever `ask` vacuumed up next. - Enter-steer with attachments degrades to queue with an explanatory toast; the per-send menu routes through the same composer-aware wrappers. - The drain and sendQueuedNow pass the item's files as `overrideFiles`; media items never steer (send as a normal turn when idle, re-front otherwise). ask() no longer clears composer state for caller-supplied overrideFiles — only regenerate keeps that behavior. - During-run submits hold while uploads are in flight, mirroring the send button's filesLoading gate; queued chips show a paperclip count. * 🎛️ feat: Rework During-Run Chips into Action Rows Full-width rows above the composer (reference-UI parity): each queued message shows a primary Steer/Send-now action, delete, and a "…" menu with Edit message (restores text + attachments into the composer) and a Turn on queueing/steering toggle that flips the Enter default. Steer rows share the layout with status text; failed steers keep retry / edit / queue-convert. The per-send menu gains the same default toggle. Queued file refs now retain filename + bytes so edit-restore rebuilds real composer entries (draft-recovery shape). * 🖇️ feat: Steer With Attachments (Multimodal Mid-Run Injection) Steering now carries media end-to-end instead of degrading to queue: - The steer POST accepts sanitized attachment refs (cap 10; only file_id is trusted — the drain re-fetches owner-scoped and re-derives everything else). SteerQueueItem/TPendingSteer/SteerContentPart carry `files` refs; encoded data is never persisted or queued. - New api/server/services/Files/steering.js decouples attachment building from the request path: encodeSteerContent reuses the exact per-turn pipeline (addFileContextToMessage + processAttachments' single-pass categorize/encode, SDK formatMessage assembly, prependFileContext for extracted text) with zero new encoding code. buildSteerMedia feeds the drain hook's new buildMedia seam (any failure degrades that steer to text-only — words always land); stampSteerPartMedia re-encodes past steer parts per turn with ONE batched owner-scoped fetch and stamps a transient `media` array, replaced immutably so it can never leak into a save. Replay honors resendFiles like regular message media. - The SDK's formatAgentMessages (the formatter agents actually use) gained the steer replay branch on the PR branch; the local formatMessages.js branch now mirrors the media preference. - Client: steerFromComposer consumes composer files into the POST, chips/seeding/conversions carry files everywhere (retry, queue convert, abort/error recovery), queued media items steer for real, and SteerBubble renders the steered attachments inline. * 🧵 fix: Harden Steer Recovery Races and Drain Isolation Codex round 3 (7 fixes): - A 202 ACK landing after the run ended converts straight to a queued follow-up (server queue is gone; no event will ever resolve a pending chip for a finished run). Covers stream errors with in-flight POSTs. - A Stop that lands pre-completion can arrive as a final with unfinished:true and no aborted flag — runEnd now treats it as aborted so queued messages are not auto-sent against the user's Stop. - Leftover-steer conversion merges chronologically by createdAt instead of appending, preserving the order the user composed. - Auto-drained queued messages pass explicit (possibly empty) overrideFiles/overrideQuotes/overrideManualSkills: a drain can no longer vacuum up files, quotes, or skill picks staged in the composer for the user's NEXT message (ask() treats overrideFiles != null as authoritative). - Failed-steer Retry and resume-on-load chip restoration keep the steer's attachments. - The job-replacement guard moved INSIDE the store's atomic drain/close-and-drain (Lua createdAt compare; in-memory equivalent): a stale run's hook or finalization can neither consume, close, nor steal a replacement job's steer queue, and the drain hook drops its separate check-then-drain round trip. * 🧰 refactor: Typed Steer Controller, Single-Query Media Pass, Round-4 Fixes Codex round 4 + efficiency tightening in one pass: - Moved the steer guard ladder (validation, file sanitization via a shared toSteerFileRef picker, ownership/tenant checks, status-guarded enqueue) into packages/api as handleSteerRequest; api/steer.js is now a thin wrapper. Ladder covered against the REAL in-memory job manager in request.spec.ts; the api spec pins only the wrapper contract. - Folded the steer replay stamp into the turn's ONE historical-files query: collectHistoricalFileRefs also gathers steer-part refs, the owner-scoped doc map rides client state, and stampSteerPartMedia consumes it (no second round trip) while encoding parts in parallel. - Stamped steer media now counts against the run budget (existing multimodal counter over the non-text parts, folded into indexTokenCountMap/promptTokens after the stamp). - Steer route runs the PII filter BEFORE moderateText, matching chat.js so blocked sensitive text never reaches the external moderation API. - Interrupt & send survives the abort-response-beats-SSE-final race: stopGenerating writes the run-end signal itself when the one-shot interrupt flag is armed and no signal landed (double-fire safe). - Resume reconciles chips against the server's still-queued list even when EMPTY, clearing chips for steers applied while disconnected. - The local formatter's steer flush preserves non-text assistant parts (array-content AIMessage) instead of folding to text. * 🔒 fix: Replay-Aware Capability Gate and Round-5 Race Closures - isSteeringSupported now requires BOTH halves of the SDK contract: injection (HOOK_INJECTED_MESSAGES_CAPABLE) AND replay (ContentTypes.STEER, shipped in the same SDK commit as the formatAgentMessages steer branch). An SDK that can inject but not replay 501s the steer route — no release window can create steer parts that would leak into provider-facing assistant content. - The local formatter mirrors the SDK's anchor reset: a post-steer tool_call mints a fresh AIMessage instead of attaching to the pre-steer anchor (invalid provider ordering). - Queued-chip send-now and the NO_ACTIVE_RUN fallback pass explicit (possibly empty) overrideFiles so an idle send can't vacuum composer files staged for a different draft. - Redis createJob deletes the stale steer list BEFORE the replacement hash is written as running — a steer 202-accepted against the new job can never be wiped by the reset. - Resumed-turn finalization mirrors the normal path's terminal drain: createdAt-guarded close-and-drain, leftovers ride the resumed final event as pendingSteers instead of being cleared by completeJob. - buildSteerMedia restores composer order over the $in result so multi-attachment steers reach the model in the order the user saw. * ⚛️ fix: Atomic Job Replacement and Boundary-Clean Steering Module Codex round 6 (5 fixed, 1 standing deferral): - createJob resets the steer queue and writes the job hash in ONE same-slot Lua script (JOB_CREATE_LUA): a steer POST can no longer interleave between them on cluster, so a steer accepted against one run can never be drained into another. Redis-validated. - The steering media pipeline moved to packages/api (agents/steering/media.ts) with injected getFiles and a structural client interface — /api keeps zero steering logic; specs ported to the DI seam. - handleSteerRequest checks the job BEFORE the capability gate: a steer racing completion on an unsupported SDK gets 404 (send-now) instead of a 501 queue with no run-end signal left to drain it. - useQueueDrain binds to the active conversation: navigating away between the final SSE and the drain effect leaves the signal unconsumed instead of submitting A's follow-up into B; the drain fires on return. - abortJob closes and drains the steer queue BEFORE the content snapshot, so a drain-hook apply that lands pre-drain is captured inline rather than lost between the snapshot and the terminal drain. * 🚦 fix: Parked Run-End Signals, Interrupt Priority, Settled-Run Fallbacks Codex round 7 (5 fixes): - Run-end signals for a non-active conversation are PARKED per conversation instead of squatting the shared index slot: a later run finishing on the same pane can no longer overwrite them, and the parked drain fires when the user returns. - "Interrupt & send" front-inserts carry a priority flag that outranks createdAt when abort leftovers merge back chronologically — the urgent redirect drains first, not the oldest steer. - STEER_UNSUPPORTED/RUN_PAUSED/QUEUE_FULL rejections landing after the run settled mirror the NO_ACTIVE_RUN fallback and send immediately (queueing would strand the text with no run-end signal left); on the pinned SDK this is the common Enter-near-run-end path. - A failed abort (e.g. 404 when the run completed first) still signals the interrupt drain, so the queued interrupt message can't strand and the armed flag can't leak onto a later run. - Steered-image fallback alt text is localized (com_ui_attached_image). * 📌 chore: Adopt Published @librechat/agents Types Post-Bump dev's pin bump to ^3.2.62 (the release carrying injection + steer replay) landed via merge; the steering runtime now uses the SDK's real InjectedMessage/hook-output types instead of the local structural mirrors that bridged the pre-publish window. The two-half capability probe stays as the defensive gate for mismatched deployments — and the capability spec now exercises its TRUE path against the published package in CI. * 🛅 feat: Park-and-Claim Steer Recovery + Host-View Content Reads Codex round 8 (6 fixed incl. both P1s, 1 push-back): - The long-deferred no-subscriber gap is closed: every terminal drain (final, aborted-final, error, abortJob, resumed finalize) PARKS acknowledged leftovers on the job hash (unrecoveredSteers), and the status route claims them exactly once for inactive jobs — a client that closed/reloaded past the transient final event restores its steers as queued chips within the post-terminal TTL. A replacement run clears the parked copy (a live client started it). - Same-instance content reads are steer-complete: RedisJobStore now caches the HOST content array (WeakRef) via setContentParts and prefers it over the SDK graph cache, whose view never contains host-authored steer parts; the graph fallback splice-INSERTS steer chunks at their recorded host-view indices (the graph array is unshifted, so assignment would overwrite SDK parts). - Replay token accounting now counts prepended file-context text: full stamped content minus the steer body (already counted), so large steered documents hit the budget instead of bypassing pruning. - The queue drain restores an item when ask() refuses without sending (history not yet in cache after navigating back) — text is never silently dropped. - The armed interrupt flag travels WITH a parked run-end signal, so another run on the same pane can neither consume nor clear it. - parseTextParts extracts steer text (search indexing / audio). * 🎛️ refactor: Single Send Slot + In-Thread Steer Messages - Merge the during-run send affordance into the send/stop button slot: with composer text the send button replaces Stop (Enter = default action), hover reveals Steer/Queue/Interrupt rows with shortcuts; drop the separate DuringRunActionsMenu chevron - Add during-run keyboard chords: Cmd/Ctrl+Enter = non-default action, Alt+Enter = interrupt & send (plain-Enter submitters only) - Render steers as standard user messages in the thread: SteerPart (icon + author header + user text presentation) replaces the SteerBubble, and submitted steers appear immediately at the projected injection point via the PendingSteers slot on the streaming message - Keep composer rows only for recoverable states: failed steers (retry/edit/queue) and queued follow-ups * 🩹 fix: Keep the Replacement Submission Alive Across Abort Settlement The aborted run's final SSE event fires before the abort HTTP response resolves, so an armed interrupt & send drains and starts the NEXT submission while the abort POST is still in flight. The response handler's unconditional clearAllSubmissions() then reset the new submission, aborting its stream attach before the subscribe — the follow-up ran and persisted server-side but the live placeholder finalized empty (content appeared only after reload). useAbortCleanup captures the submission before the abort round-trip and both settlement paths (success and 404-catch) clear only when the captured submission is still current; a replacement stays untouched. Plain Stop behavior is unchanged. * 🧭 test: Playwright E2E for Mid-Run Steering and Queuing - Add e2e/specs/mock/steering.spec.ts: steer mid-run (202 + immediate in-thread pending part + real MCP tool boundary + words survive run end), Cmd/Ctrl+Enter queue with auto-send after clean completion, and Alt+Enter interrupt & send with the follow-up streaming into the live view - Add the E2E_STEER_TOOL_REPLY fake-model marker: slow preamble, a real remember_fact MCP tool call (PostToolBatch boundary), then a final turn - Test 1 pins the run-end degradation contract while the SDK's top-level agentId stamping bug blocks live injection; its header documents the assertions to flip once the fixed SDK is pinned * 🧷 fix: Job-Independent Steer Recovery + Expiry and Resume-Gap Parking Codex round 10: the park-and-claim recovery had lifecycle holes. - Move parked steers off the job hash onto their own bounded-TTL store key (JOB_CREATE_LUA resets it; deleteJob leaves it alone): the default completeJob path deletes the job record immediately, and the Redis read path never deserialized the old hash field — recovery previously worked only with STREAM_KEEP_COMPLETED_JOBS on the in-memory store - Carry the owner identity inside the parked payload and authorize the claim against it, so the status route recovers steers on its jobless branch too (the common reload-after-terminal case); a non-owner claim returns nothing and re-parks the payload - Park queued steers on approval expiry: snapshot the frozen queue before the requires_action→aborted CAS (whose terminal cleanup drops the steers key) and park only when the CAS wins - Mirror the terminal drain/park block in resume.js's failure path, which previously let completeJob's backstop clear 202-accepted steers - Close the Redis snapshot→subscribe resume gap: re-peek the queue after attaching and re-surface missed on_steer_applied events from the durable content view (synthesizeAppliedSteerEvents), updating resumeState.pendingSteers to the live queue * 📌 chore: Require @librechat/agents 3.2.63 + Applied-Steer E2E Contract - Bump the @librechat/agents pin to ^3.2.63 in api/ and packages/api/: it scopes the hook agentId marker to subagent child graphs, so the steering drain hook fires at top-level tool-batch boundaries and mid-run injection is active (danny-avila/agents PR 307) - Flip e2e steering test 1 from the documented degradation contract to the applied-steer contract: the optimistic in-thread part transitions to the persisted part at the tool boundary and survives inside the response after run end, with no queued follow-up turn * 🎗️ feat: Steered Messages Join the Message-Nav Ribs Steers are user messages, so they get their own clickable rib on the navigation rail, interleaved at their in-thread position inside the response that absorbed them (one DOM query in document order). SteerPart anchors itself as #steer-<id> with a steer-render marker — both the optimistic pending entry and the persisted part — and the rib carries the user role label with a preview drawn from the steer's text body, skipping the author header. * ❎ feat: Cancel a Queued Steer Before Injection + True User-Message Alignment - Add POST /chat/steer/cancel: removes ONE still-queued steer by id via an atomic list rebuild (Redis Lua preserves order and TTL), authorized against the job owner; removed:false is advisory — the cancel lost its race to the drain or the run end, never an error - Surface an × on the in-thread pending steer (server-acknowledged entries only): optimistic removal, restored if the POST fails since the server would still inject the words - Outdent SteerPart past the response's icon column so steers sit flush with top-level message rows, reading as regular user messages * 🧯 fix: Round-11 Recovery Hardening + Provider-Free Pending Slot - Reconcile the resume steer gap by steerId SETS, not queue length — a steer added in the gap (or an equal-length drain+enqueue swap) now refreshes resumeState.pendingSteers and still synthesizes the missed on_steer_applied events - Make completeJob's terminal backstop park: direct error-path callers without the controllers' close-and-park no longer silently clear 202-accepted steers (createdAt-guarded closeAndDrain + owner park before the terminal write) - Persist the steer part BEFORE media encoding in the drain hook: an abort inside the encode window can no longer lose a file-steer (the part refs come from the enqueue-sanitized item; replay re-encodes per turn unchanged) - Move the parked-claim owner check INSIDE the atomic store claim (substring gate in the Lua / in-memory equivalent): a non-owner probe can no longer transiently delete the recovery payload; the app-side parse stays authoritative - Park queued steers in BOTH stores' own requires_action expiry cleanup, which bypassed the manager-level sweep - Sweep expired parked steers from the in-memory store's periodic cleanup; restore a queued chip when send-now's submit is refused; upsert steer ACKs so an SSE reconnect reseed cannot duplicate chips - Mount the cancel mutation per steer item so the pending slot needs no QueryClient on ordinary streaming renders (fixes the CI failure in ContentParts.integration.test) - Skipped delivery-gated parking (finding 8): transport receiver counts cannot prove browser delivery, and gating the only durable copy on them trades cosmetic chip resurrection for real text loss; the window is already bounded by claim-on-read, createJob reset, and the TTL * 🩺 fix: Annotate PARKED_STEERS_TTL_MS for isolatedDeclarations tsdown's d.ts generation requires explicit types on exported consts with computed initializers; tsc --noEmit does not run that check, so the round-11 export slipped past local verification and broke Build packages (and every downstream CI job that consumes the built dist). * 🛟 fix: Round-12 Terminal-Path Recovery + Durable Steer Events - Park queued steers before the stale-running reap deletes a crashed or hung job in BOTH stores — the one terminal path with no controller finalization; requires_action expiry parking refactored onto the same snapshot/park helpers - Enqueue instead of dropping when a steer fallback send is refused: both the NO_ACTIVE_RUN branch and the settled-run rejection branch now observe sendNow's false return - Recover on the SSE reconnect-404 terminal path: convert local pending steers to queued, claim parked steers via /chat/status, and write a non-completed run-end signal so interrupt flags release without auto-sending an unknown outcome - Fall back to a positive parked-recovery TTL when completedTtl is 0 (SET EX 0 is invalid and silently killed recovery) - Make on_steer_applied durable before publish: emitChunk gains a durable option that awaits the chunk-log append (best-effort) ahead of the transport publish; the default delta path stays fire-and-forget * 🔐 fix: Round-13 Steer Authorization + Trusted File Refs - Resolve client-supplied steer file refs against the DB owner-scoped at enqueue and queue only DB-derived shapes (same filter as the injection fetch, shared via refs.ts); any unresolved id fails loud with 400 — spoofed type/filepath metadata can no longer be persisted into assistant content or rendered in chat/share views - Enforce agent authorization on /chat/steer against the ORIGINATING run's job identity: the chat path's role gate (AGENTS:USE, with the same non-agents-endpoint skip) plus the per-agent ACL check with the capability bypass — revoked access mid-run can no longer inject; cancel stays ownership-only (nothing model-bound) - Mark steered uploads used after a successful enqueue (owner-scoped, best-effort) so the upload-window TTL cannot reap a file the persisted steer part references - Consume the parked recovery copy after live delivery: converting final/abort/error pendingSteers fires one owner-gated claim-on-read, so dismissed chips can no longer resurrect on a later reload * 🎙️ fix: Round-14 Composer-Context Fidelity + TTS and Queue-State Gaps - Keep steer text out of generic assistant text extraction: parseTextParts excludes STEER parts by default with an includeSteer opt-in for the full-record surfaces (Meili indexing, aborted-response persistence) — TTS callers no longer speak the user's own mid-run words - Mark queued uploads used at enqueue time via a minimal owner-scoped POST /files/usage (fail-closed without a user; upload limiters do not apply to a metadata touch), fired once wherever composer files enter the queued state — the upload-window TTL can no longer reap a file waiting out a long run or approval pause - Carry quote chips and manual skill picks on queued items: captured and consumed from the composer at queue/interrupt time exactly like files, threaded through the drain and send-now overrides, and restored by the queued row's Edit message - Key an early-aborted FIRST turn's run-end signal to NEW_CONVO (resolveRunEndTarget) so queued follow-ups stay visible on the restored new-chat composer instead of parking under an optimistic stream id the user never sees again * 🧿 fix: Round-15 Gap Coverage + Consolidated Sweep (Share Leak, Abort Ids, Chip Hygiene) - Run the resume steer-gap check for every still-active job: an empty snapshot no longer skips the re-peek, and synthesis now keys on the FRESH content view so an applied-in-gap steer that was never snapshotted still re-surfaces (over-emission is benign — applied-id dedupe, index-stable parts) - Thread queued context through steer degradation: sendQueuedNow passes the item's quotes/skills into submitSteer, and every fallback (requeue or settled send) restores them instead of dropping to text+files - Stop shared links from leaking steer attachment refs: the share snapshot now walks content — files-excluded shares strip steer-part files entirely; files-included shares sanitize and share-route them like top-level files (copy-on-write, non-steer content by reference) - Seed pending-steer chips unconditionally on load/return so a steer applied while away cannot linger as a stale chip beside its part - Use the abort response's resolved job id: chips/drain-signal land where the user actually is (NEW_CONVO for a new-held first turn, consistent with resolveRunEndTarget) while the parked-copy claim hits the resolved id instead of a no-op /chat/status/new - Open steered documents like normal message files (FilePreviewDialog) - Cap the applied-steer id set on the live path via a shared helper; kept surviving run end deliberately (late-ACK race depends on it) and fixed the atom comment that claimed otherwise * 💡 fix: Un-light Steer Ribs When Their Node Is Replaced Two stacked gaps kept a steer rib lit after scrolling away: the pending→applied swap replaces the DOM node under the same id, which produces no IntersectionObserver exit and — because the entry list dedupes on (id, preview) — no entries change either, so the observer kept watching a detached node; and the rail's mutation filter only reacted to .message-render nodes, so steer-node swaps and removals never triggered a refresh at all. - reconcileObservedElements re-points the observer at replaced nodes from the mutation-driven refresh regardless of entries identity, dropping stale visibility until the fresh node reports (the observer fires its initial intersection immediately, so a truly visible part re-lights within a frame) - The mutation filter now recognizes steer-render nodes alongside message rows * 🪪 fix: Round-16 Recovery Owner Fields + Context Stickiness + Share Labels - Park resumed-run leftovers with the manager facade's metadata owner fields: a bare job.userId is undefined on that shape, which made every parked payload from a resumed HITL run unclaimable - Keep a queued item's quotes/skills sticky through a successful steer ACK: the pending chip carries them (client-only), reseeds preserve them across reconnects, and every terminal conversion — local or server-list, merged by steerId — restores them onto the queued item - Convert resumeState.pendingSteers on the inactive status branch (deduped against unrecoveredSteers) so steers observed in the expired-pause-before-sweeper window convert instead of vanishing until a later reload - Label shared steer parts share-safely via the existing ShareContext: a viewer's own name no longer appears on the sharer's steered messages * ✂️ fix: Carry Steer Context Through the Failed-Chip Edit Action Retry and convert-to-queue already preserve a failed steer's carried quotes/skills; Edit message dropped them on the way back to the composer. It now restores them through the same context path. |