Commit graph

4793 commits

Author SHA1 Message Date
Danny Avila
2d227dbc3b 🛡️ feat: Configurable Baseline HTTP Security Headers
Adds helmet's CSP-independent headers (HSTS, X-Frame-Options,
X-Content-Type-Options, COOP, CORP, Referrer-Policy) on every response,
with contentSecurityPolicy explicitly disabled. Every header that can
break a deployment is configurable, so there is no allow-list to go
stale the way #7377's hardcoded CSP directives did.

HSTS includeSubDomains defaults off rather than matching helmet's
on-by-default: it would otherwise pin every sibling subdomain to HTTPS
for a year in every visitor's browser, and undoing that requires
serving max-age=0 from each affected host.
2026-07-28 09:37:07 -04:00
Danny Avila
9c95bf445f
🍂 chore: Prune Deprecated Packages From the Dependency Tree (#14483)
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
Removes three of the eight deprecation warnings emitted on `npm install`.

- Drop `@types/winston` from `packages/api` and `packages/data-provider`.
  The published tarball ships no type declarations at all, so `winston`'s own
  types were already being used. Declare `winston` as a devDependency instead,
  since both packages `import type { Logger } from 'winston'` and were relying
  on hoisting to resolve it.
- Drop `rollup-plugin-postcss` from `packages/client`. It is unreferenced since
  the package moved to tsdown, and pulled in `cssnano -> postcss-svgo -> svgo@2`,
  which is the only consumer of the deprecated `stable`.
- Override `test-exclude` to ^8 so `babel-plugin-istanbul` stops resolving
  `test-exclude@6`, which pins the deprecated `glob@7`.

The remaining five warnings (`ldapjs`, `whatwg-encoding`, `node-domexception`,
and workbox-build's `glob`/`source-map`) are transitive with no non-deprecated
version available upstream.
2026-07-28 09:28:06 -04:00
Danny Avila
324584552c
⏱️ feat: Configurable HTTP Server Timeouts (#14481)
* http server config added

* Fix TypeScript compatibility by accepting NodeJS.ProcessEnv directly when applying optional HTTP server timeout configuration.

* fix(api): configure HTTP server timeouts for clustered workers

* 🕰️ fix: Warn When HTTP Timeouts Are Not Enforced

Codex review of the rebased contributor work surfaced two ways these settings
silently do nothing. Both reproduce, and neither was reported to the operator.

Bun accepts the four property assignments and reflects them back, but does not
enforce them: with keepAliveTimeout=100 and buffer=1000, Bun 1.3.13 held a
keep-alive connection past 3s where Node 24 closed it at 1101ms. Since `b:api`
runs the server under Bun, the existing info log confirmed a configuration that
was not in effect. Warn instead.

Node sweeps header/request timeouts on `connectionsCheckingInterval`, a
createServer option that `app.listen()` leaves at 30s, so sub-30s values round
up to it: headersTimeout=2000 returned 408 at 30004ms by default versus 2010ms
with a 250ms interval. Warn on values below the sweep interval rather than
restructure server construction, since every documented value and both Node
defaults already sit well above it. keepAliveTimeout is socket-driven and stays
exact, so it is excluded.

Both caveats documented in .env.example.

* 🩹 fix: Inject Runtime Versions Instead of Mutating `process.versions`

The spec deleted `process.versions.bun` to reset between cases, which failed
typecheck with TS2790: `@types/bun` is a packages/api dependency and augments
NodeJS.ProcessVersions with a required `bun: string`, so the property is not
optional and cannot be deleted. Assigning undefined would fail for the same
reason.

That augmentation also made the production check dishonest: TypeScript saw
`process.versions.bun` as always a string, so `!= null` read as a no-op branch
even though it is correct at runtime under Node.

Both resolved by taking runtime versions as a third injectable parameter,
matching the existing `environment` parameter. Callers in api/server are
unchanged, the narrow `{ bun?: string }` type restores honest narrowing, and
the tests no longer mutate global state, so they assert the same behavior
whether the suite runs under Node or `bun jest`.

* 📏 fix: Stop Claiming a Ceiling on Sweep-Delayed Timeouts

The warning added in 9adc3eb1c said the effective timeout is "up to 30000ms",
which promises a bound that does not hold. Node detects header/request expiry
only on the next connection sweep, so the delay is relative to the deadline
rather than capped by the interval: measured against the default 30s sweep, a
2000ms headersTimeout closed at 30004ms, 15x the configured value, and cases
where the timeout is near the interval did not fire within a 9s window at all.

Reworded to state the mechanism without asserting a ceiling, and to point
operators at values of 30000ms or above for predictable enforcement. Same
correction applied to the .env.example note, which claimed short values "round
up" to the interval.

*  fix: Clamp Headers Timeout to the Request Timeout

Setting only HTTP_REQUEST_TIMEOUT_MS below the 60s headers default left
headersTimeout > requestTimeout, a pairing createServer rejects outright with
ERR_OUT_OF_RANGE. Assigning the properties after construction skips that
validation, and the mismatch silently defeats the request timeout for a stalled
body: with requestTimeout=4000 and headersTimeout at its 60000 default, a client
that completed its headers and then stopped mid-body was still connected after
12s. Clamping headersTimeout to 4000 closes the same connection at 4017ms.

An earlier round dismissed this after testing partial *headers*, where
requestTimeout does evict on time. The gap only appears once headers are
complete and the body stalls, which is the case these timeouts exist to bound.

Mirrors Node's own rule rather than its constructor default: zero on either side
means disabled and is left alone, and an explicitly configured
HTTP_HEADERS_TIMEOUT_MS that conflicts is warned about before being clamped
instead of failing startup over a config typo.

---------

Co-authored-by: Peter Rothlaender <peter.rothlaender@ginkgo.com>
2026-07-28 09:10:17 -04:00
Danny Avila
f7bc50ae5b
📦 chore: bump @librechat/agents to v3.3.4 (#14482)
Some checks failed
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
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
Publish `@librechat/data-schemas` to NPM / publish-npm (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
2026-07-28 08:36:18 -04:00
Dustin Healy
044c134ecf
🤏 fix: Filter Admin Config Reads by Section-Scoped Read Capability (#14472)
listConfigs, getBaseConfig, and getConfig only checked the broad
read:configs capability, so a caller holding nothing but
read:configs:<section> grants got a blanket 403 on all three instead
of a response filtered to the sections they hold. Any deployment
using section-scoped config grants hits this.

Adds hasAnyConfigReadAccess as a cheap pre-flight check covering
broad and section-scoped read and manage grants (manage implies
read), so a zero-access caller still 403s before a DB fetch while a
section-scoped caller gets the response filtered to exactly what
they hold. The same manage-implies-read rule is fixed at its root in
getParentCapabilities so a manage-only caller sees the section they
manage instead of having it stripped after passing the pre-flight.

Resolves every section for a request in one batched
getHeldCapabilities query via getReadableConfigSections instead of
one round trip per section.

Includes AppConfig field-renaming normalization (interfaceConfig,
turnstileConfig, mcpConfig) so the filter checks the canonical
section name rather than the renamed response field, and stops
availableTools from bypassing the filter by gating it on its
filteredTools/includedTools source sections.
2026-07-28 07:38:37 -04:00
Danny Avila
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 b687922.

The hold derived each new deadline from `Date.now()`, so a caller touching
once a day advanced it by another 24h every time, far below the rate limit.
That left indefinite preservation reachable and made the PR's replay claim
wrong: the window was bounded per call but not in aggregate.

Anchor the deadline to the file's immutable `createdAt` instead of the
request clock. `extendFilesTTL` now takes a lifetime and sets
`expiresAt = max(expiresAt, createdAt + holdMs)` in an aggregation
pipeline, so the target is a fixed point per file and replay is inert
rather than merely bounded. `$max` keeps the widen-only property and the
`expiresAt: {$exists: true}` filter still refuses to resurrect a released
TTL; `createdAt: {$exists: true}` fail-closes when the anchor is absent.

The update runs with `timestamps: false`: a hold is TTL bookkeeping, not a
content write, and bumping `updatedAt` also made every re-touch count as a
modification, hiding whether the deadline actually moved.

Also drop four `.node_modules-*` symlinks that `git add -A` swept in from
an npm install. They pointed at absolute paths on one machine, so every
other checkout got dangling entries. Added the pattern to .gitignore so a
workspace install cannot reintroduce them.

* 🔒 fix: Track the configured approval window in the `/files/usage` hold

Codex review on 9277620.

`endpoints.agents.checkpointer.ttl` is a positive int with no upper bound,
and its docs invite raising it for longer review windows. It drives the
pending-action expiry, so a run can legitimately stay paused past 24h. The
fixed 24h lifetime would then let Mongo reap an attachment while its
approval was still live, and the later queue drain would send a file that
no longer exists.

Replace the fixed constant with `resolveFilesUsageHoldMs`, which adds the
configured approval window to a 24h baseline covering upload, enqueue, and
the run reaching its pause. The route reads the window from the same
`getApprovalTtlMs(checkpointerCfg)` the pending action uses, so the two
stay in lockstep.

The replay bound is unaffected: the window is a per-deployment constant and
the deadline is still `createdAt + holdMs`, so a replayed touch re-asserts
the same instant and `$max` skips the write. Only an operator config change
moves it, never a client.

* 🔒 fix: Renew the `/files/usage` hold across queued runs, under a ceiling

Codex review on 2bd3c52.

The drain sends one queued item per run completion, and each item starts a
run that may itself pause for the full approval window. Since the hold was
taken once at enqueue and pinned to the upload time, an item several places
back could sit through multiple approval windows and lose its attachment
while its chip and the live approval were still there. Another regression
from this PR: the old `$unset` made retention permanent, so deep queues
happened to work.

The queue is unbounded, so no fixed lifetime covers it. Split the hold into
a renewable window and a ceiling:

  expiresAt = max(expiresAt, min(now + renewMs, createdAt + maxLifetimeMs))

`renewMs` covers one run's wait and is granted from now, so a queue that is
still draining re-asserts it at each transition; `useQueueDrain` now marks
the remaining items' files whenever it pops one. `maxLifetimeMs` is
measured from the immutable upload time and clamps every renewal, so
repeated touches converge on a ceiling instead of advancing per call, which
keeps the replay bound from the previous round intact.

This also tightens abandonment: a queue nobody drains now lapses one
`renewMs` after its last touch instead of surviving to the ceiling.

`useQueueDrain`'s spec gained a QueryClientProvider, since the renewal goes
through react-query.

* 🔒 fix: Renew queued holds on a heartbeat, and stop dropping batches

Codex review on f616bed.

Three gaps in the renewal added last commit:

- `collectQueuedFileIds` returned early at the server's 10-id cap, so a
  remainder holding more than one batch renewed only its first message and
  left the rest on their enqueue-time hold. Collect everything and split
  into capped requests instead of truncating.
- A refused `ask()` restores the popped item, but renewal ran before the
  send and covered only the pre-existing remainder. Since the run-end signal
  is already consumed, nothing would touch that item again. Renewal now runs
  after `ask` and includes the restored item.
- A single run can interrupt for approval more than once, each pause running
  to the configured window, so renewing only at drain transitions leaves a
  gap longer than `renewMs` with no renewal in it. The ceiling cannot help
  when nothing renews.

The third is the same structural gap as the previous round along a new axis:
renewal tied to discrete events loses the file whenever two events are
further apart than the hold. Rather than hook each transition, renew on a
30 minute heartbeat while anything is queued, which is far below the
smallest hold (24h) and so covers any single gap regardless of cause.

Still bounded: every renewal is clamped against the file's upload time, so
the ceiling is unchanged. A queue nobody has open emits no heartbeat and
lapses one `renewMs` after its last touch, preserving the abandonment
behaviour.

* 🔒 fix: Cover the pre-migration queue, first tick, and `/usage/`

Codex review on 892a27d.

- The heartbeat watched only the active conversation id, but `drainNext`
  merges in the `NEW_CONVO` queue, which outlives the URL update: items
  queued during the first turn stay keyed there until that run ends. It now
  renews the union of both, deduped since they are the same atom before
  migration.
- The interval installed without firing, so returning to a conversation
  whose hold was nearly up waited out a full period before the first
  renewal. It now renews immediately, then on each tick.
- Express's non-strict routing sends `POST /files/usage/` to the same
  handler with `req.path === '/usage/'`, so the exact comparison pushed it
  onto both upload limiters. A trailing-slash client would have spent its
  upload quota, and collected file-upload violations, on metadata
  heartbeats. Matching now tolerates the trailing slash.

Firing on effect start also made the drain-time renewal redundant: popping
an item changes the held set, so the renewal effect re-runs on its own. The
one case it cannot see is a refused send, where restoring the item leaves
the set identical, so that branch keeps an explicit renewal and the rest is
removed. Net one request per transition instead of two.
2026-07-28 07:37:26 -04:00
Danny Avila
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.
2026-07-28 07:32:44 -04:00
Danny Avila
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
2026-07-27 18:51:03 -04:00
Danny Avila
e0892bb291
🍃 fix: Strip $-Prefixed Schema Keywords Before Persisting MCP Tool Params (#14464)
* 🐛 fix: strip $-prefixed schema keywords from MCP tool params before storage

MCP tools whose inputSchema carries a spec-compliant $schema keyword (or any
other $-prefixed JSON Schema keyword) failed to register: MongoDB rejects field
names beginning with $, so persisting the tool's parameters blob threw
"The dollar ($) prefixed field '...$schema' is not valid for storage".

Normalize the schema when building stored toolFunctions (resolve $refs and drop
$-prefixed keywords via the existing resolveJsonSchemaRefs + normalizeJsonSchema
pipeline). normalizeJsonSchema now strips every $-prefixed keyword, not just
$defs, while preserving property names that happen to start with $.

* fix: recurse through every schema-valued keyword when stripping $ keys

MongoDB rejects $-prefixed field names at any depth, but the normalizer only
recursed through properties, items, additionalProperties and unions, so a
$schema or $comment nested under not, if/then/else, contains, propertyNames,
patternProperties, dependentSchemas or prefixItems survived into the persisted
tool parameters and still failed registration.

The keyword sets are now explicit, covering the single-subschema, map-of-schema
and list-of-schema forms.

A $-prefixed property name is deliberately left alone: it is an argument the
tool actually accepts, so dropping it would silently remove the parameter from
the schema the model sees.

* fix: recurse into draft-07 dependencies and 2020-12 contentSchema

Both are schema-bearing and were absent from the traversal sets, so a nested
annotation survived into the persisted tool parameters and still hit the
MongoDB dollar-prefixed-field failure. dependencies is polymorphic - a value
may be an array of property names rather than a subschema - and that form
round-trips unchanged.

* fix: keep __proto__ entries when normalizing schema maps

Schema-map keys name instance properties, so __proto__ is a legal entry and
arrives as a real own property via JSON.parse. Plain assignment invoked the
prototype setter instead, silently dropping the constraint; entries are now
defined rather than assigned.

* fix: bound MCP schema reference expansion and keep __proto__ arguments

A remote MCP server controls the schema fetched at registration, and sibling
references to the same definition each re-expand because visited is cleared
after resolving - so an acyclic graph where each Dn holds two refs to Dn-1
expands 2^n. At depth 24 that is over 16 million nodes, enough to block the
event loop or exhaust memory before registration finishes.

Resolution now carries a node budget and leaves a reference unexpanded once it
is spent, and assignments use defineProperty so an argument legitimately named
__proto__ is not swallowed by the inherited setter during resolution.

---------

Co-authored-by: Arham Wani <arhamwani765@gmail.com>
2026-07-27 18:50:34 -04:00
Danny Avila
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>
2026-07-27 14:45:38 -04:00
Danny Avila
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.
2026-07-27 12:45:55 -04:00
Danny Avila
531fecc82b
🍃 chore: Bump Mongoose to 8.24.1 to Patch Prototype Pollution (#14461)
Closes GHSA-664h-wqgq-64gw (CVSS 6.5, CWE-1321), a prototype pollution
in update casting via a __proto__-prefixed dotted path. Affected range
is >=8.0.0 <8.24.1, so 8.23.1 was flagged by npm audit.
2026-07-27 12:40:14 -04:00
Danny Avila
a53936d273
🧭 test: Cover Agent Handoffs End to End (#14428)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* test: cover agent handoffs end to end

* style: sort handoff imports

* fix: normalize missing agent handoff edges

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

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

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

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

* fix: normalize invite email before token creation

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

---------

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

* fix: preserve tool approval state across resume

* fix: preserve agent context in mock stream responses

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

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

* test: align Redis stream readiness assertions

* perf: overlap remaining agent startup work

* perf: persist initial agent job metadata atomically

* test: add agent startup latency benchmark

* fix: harden resumable agent stream lifecycle

* fix: isolate replacement stream lifecycles

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

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

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

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

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

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

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

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

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

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

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

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

Two issues raised in review:

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

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

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

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

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

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

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

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

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

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

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

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

* chore: sort stream regression imports

* test: mirror sliding sequence ttl in publisher mock

* fix: prevent duplicate early stream replay

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Address Codex review findings on the virtualized search results view:

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

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

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

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

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

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

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

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

Address the fourth Codex round on virtualized search:

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

* 🩹 fix: Resolve CI breakage from bundled dependency bumps

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

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

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

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

* fix: expose deployment skills to agent viewers

* refactor: centralize deployment skill ID merging

---------

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

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

* fix: Harden SSE file upload lifecycle

* style: Sort data provider imports

---------

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

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

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

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

Resolves #14287

* 🧹 chore: Remove Orphaned com_agents_enable_memory i18n Key

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

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

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

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

*  test: Cover Composer Subscription Refactor's Behavioral Changes

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

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

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

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

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

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

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

Address three P2 findings on the idempotency path:

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

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

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

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

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

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

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

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

Tests cover both age branches.

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

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

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

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

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

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

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

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

Adds a controller test asserting completeJob precedes releaseGeneration.

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

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

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

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

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

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

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

Fixes #14245

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

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

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

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

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

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

* 🧭 fix: Gate Stale-Cache Sends During Revalidation and Honor disableFocus
2026-07-20 22:31:44 -04:00