* 🧊 feat: Persist Context Fading Tier Across Agent Runs
Carry the pruner's latched context-fading tier from `@librechat/agents`
through `contextMeta` the way `calibrationRatio` already travels, so a
historical tool result keeps the same truncated bytes from one run to the
next and Anthropic's prefix-based prompt cache survives across turns
(danny-avila/agents#497).
- Add `IAgentFadingTier` to the message and conversation `contextMeta`
types, schemas and the data-provider zod schema
- Add `isAgentFadingTier`, `resolvePersistableFadingTier` and
`resolveRunContextMeta` in `packages/api`; a tier is persisted once it
carries information (masking active or a budget below the window), and
a latched tier is kept even at a neutral calibration ratio
- Seed `createRun({ fadingTier })` from the parent response's contextMeta
regardless of encoding, since caps are character-based; the field rides
the existing forward-compatible `runConfig` variable and older SDK
versions ignore it, as does `Run.getFadingTier` being absent
- Accept a valid `fading` field in event actor context and reject a
malformed one, matching the calibration checks
- Capture run context meta through one module-level helper for both
completion paths instead of two inline copies
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Seed Fading Tier Across HITL Resume, Validate at the DB Layer
Address review findings on fading tier persistence:
- Leave the "is this tier worth persisting" decision to the SDK, whose
`Run.getFadingTier()` now returns only informative tiers; comparing
against the client's window misclassified untouched conversations
whenever a reserve ratio was configured
- Carry `contextMeta` through the HITL pause projection (staged approval,
job metadata, resume state) and seed the rebuilt client from it, so a
paused turn resumes with the same tier and calibration instead of
re-deriving a shallower one and rewriting the prefix
- Share run seeding between `chatCompletion` and `resumeCompletion`, and
make capture tolerate partial runs and bare resume contexts
- Validate `contextMeta.fading` in `commitAgentEventActorState` with a
shared `isAgentFadingTier` guard in data-schemas, so a malformed tier is
rejected at the DB layer instead of silently cold-starting the actor
- Reuse `IAgentEventActorContextMeta` for `IMessage.contextMeta` instead of
extending an inline duplicate
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 test: Cover Resume Seeding From Paused Context Meta
Assert the resume controller seeds the rebuilt client from the context meta
captured at the pause before rebuilding the run, and move the capture
helper's JSDoc back above its function.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧹 style: Sort Imports in Fading Tier Plumbing
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Persist Fading Tier Through Redis Jobs and Branches
Address Codex and Copilot review findings on fading tier persistence:
- Deserialize `contextMeta` in `RedisJobStore`, whose explicit read mapper
rebuilt every other pause field but this one, so Redis-backed
deployments resumed with no tier to seed after every HITL pause
- Copy server-private `contextMeta` onto the assistant message created by
`POST /api/messages/branch`, so the next turn seeds its pruner the same
way it would from the parallel source response
- Constrain the `fading` subdocuments in both Mongoose schemas (version
enum, positive budget, required fields) to match the zod schema
- Throw a distinct "fading tier is invalid" error from both context meta
validators instead of reporting a calibration failure
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Carry Context Meta Through Stop and Re-Pause
Addresses the second Codex pass on the fading-tier persistence:
- A re-pause whose resumed segment has nothing persistable now clears the
job's `contextMeta` in the same transition instead of leaving the first
pause's calibration and tier for the next resume to seed from.
- The client publishes the run's live calibration and fading tier onto the
job after each pre-invoke context snapshot (deduplicated on value), and
the Stop path copies `jobData.contextMeta` onto the stopped response, so
a follow-up from a stopped turn keeps the prompt prefix stable.
- Mid-run captures (HITL pause, Stop) read the graph's live state; `Run`
refreshes its own getters only after the stream settles, so the pause
used to persist the seeds it started from.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Fence Context Meta Publication Ahead of Stop
A Stop handled by another replica reads the job before the owner is
signaled, so a fire-and-forget publish could lose the race, and a run
stopped before its first context snapshot never published the tier it
inherited. The client now publishes the inherited seed before the run
streams or resumes, and the context-usage handler awaits each
snapshot's publish so the write lands before the model call it
describes begins. Failures still only log; the pre-run publish is
optional-called so bare resume contexts keep working.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Read Back Context Meta After the Abort Claim
The run owner publishes its context meta ahead of each model call and
awaits the write, so a publish landing between the abort's initial job
read and its terminal claim describes the call whose partial output the
abort snapshot carries. abortJob now reads the same-epoch job back after
the claim and content re-read and takes its context meta, so the stopped
response persists the tier that produced its bytes. The refresh is
best-effort and logs on failure like the content refresh beside it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Share In-Flight Context Meta Writes and Clear Neutral Live State
Two snapshot callbacks in a parallel-agent run can capture the same tier
before the first write settles; the publisher now caches the in-flight
promise per serialized value so the second caller awaits the same
durable write instead of treating an uncommitted value as published.
A live snapshot with nothing left to persist after an earlier publish now
writes a neutral record (ratio 1, no tier), since a running job's fields
cannot be deleted through the metadata writer, so a Stop no longer
persists an earlier non-neutral state onto the response. The pre-run
seed publish is unchanged and is told apart from live snapshots by a
flag from the context-usage sink.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 chore: Drop Unused Rest Parameter in Context Meta Test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 feat: Persist Per-Agent Fading Tiers as Compact Context Meta
Reconciles the host with @librechat/agents at 9aacad1a, where graph
history stays canonical and each Run derives a provider-only projection
from the latched tiers. LibreChat therefore persists only compact state:
the calibration ratio, the default agent's tier, and now the per-agent
tiers from Run.getFadingTiers(), stored as validated entries so agent IDs
never become MongoDB field names and restored onto a null-prototype
record for RunConfig.fadingTiers. Truncated messages, canonical tool
content, projection provenance and per-message truncation state are
never persisted; the SDK's latched flag is stripped on capture.
The per-agent map travels everywhere the single tier already did:
response contextMeta, event-actor checkpoint state, HITL pause and
resume job metadata in both stores, the Stop path, and branch creation.
A shared Mongoose definition keeps the message and conversation schemas
identical, the zod schema validates entries, and commitAgentEventActorState
rejects malformed maps. Comments no longer describe graph messages as
truncated; only the provider projection is.
Tests cover: tool results and inputs reach the persisted content parts
at full size; contextMeta carries exactly the compact tier and
calibration fields; a persisted snapshot round-trips into the seeded
RunConfig unchanged and prototype-safe; per-agent tiers survive pause,
Redis job round-trips, and event-actor context validation.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Restore Event Actor Types and Publish Seeds Before Abortable Setup
The previous commit's doc rewording in the conversation types dropped the
event-actor interfaces that precede the fading tier, which broke the
declaration bundle in CI; they are restored unchanged.
The inherited context meta is now published onto the job as soon as the
parent's state is loaded and awaited at the top of chatCompletion and
resumeCompletion, ahead of run creation and the other abortable setup
stages, so a Stop during setup still persists the parent's tiers onto the
stopped response. The partial response saved when every subscriber
disconnects now carries the job's context meta like the Stop and pause
paths do. The branch route keeps the source's context meta in the saved
message but strips it from the HTTP response, matching client reads.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Serialize Distinct Context Meta Publications
Overlapping snapshots from parallel agents could carry different tiers;
each started its own write and both job stores keep whichever write
finished last, so an older snapshot could overwrite a newer one. Each
distinct publication is now issued after the previous one settles, in
call order, so the newest snapshot is what a Stop reads back. Equal
values still share one write.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Project Root Message Reads and Keep Inherited Meta on Setup Failure
The root GET /api/messages paths returned raw documents: the
single-message query read called getMessages without a projection and the
cursor page performed an unprojected find. Both now apply
CLIENT_MESSAGE_SELECT, with getMessagesByCursor accepting a select option,
so the persisted fading tiers never reach the client through the
paginated endpoint; the search-hydration read projects the same way.
A turn that loads a parent's context meta but fails before its run exists
used to replace the inherited meta with the capture of a missing run, so
the persisted error response lost the last valid tiers. Both finalizers
now keep the inherited meta when no run was created while a created run's
neutral state may still clear it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 refactor: Move Context Meta Publication Into packages/api
The publication coordinator (ordering, deduplication, in-flight sharing,
failure state) now lives in packages/api as createContextMetaPublisher,
with selectRunContextMetaToPublish deciding what a publication carries;
the legacy client keeps only the wiring to the job metadata writer. A
transiently failing write is retried with a short backoff before the
publication is reported and forgotten, so a snapshot's model call no
longer proceeds on the first rejection while the job holds the previous
record. The SDK swallows handler errors, so retrying is the only way to
strengthen the fence without failing the turn.
A terminal save now supplies contextMeta: null when the finished run has
nothing to carry, and saveMessage unsets a previously stored value in
that case, so a disconnect snapshot's record cannot outlive the run that
completed neutrally.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Widen the saveMessage Method Type for the Null Unset
The exported method interface still typed contextMeta as the message
field, so the spec exercising the null unset failed typechecking; it now
mirrors the implementation signature.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Publish Every Agent's Snapshot, Sanitize Search Hits, Unbound Agent IDs
The context-usage handler now awaits the run's context meta publish for
every ON_CONTEXT_USAGE event, hidden sequential agents included, while
the visible-snapshot bookkeeping and the client emission keep their gate;
a Stop before the next visible snapshot finds the tier a hidden agent's
model call latched. Search results are built from the hydrated hit minus
its server-private contextMeta, since Meilisearch hydration projects every
schema field. The per-agent tier entry guard no longer caps the agent ID
length: ephemeral agent IDs encode endpoint, model and sender without a
bound, and dropping such an entry would let that agent alone re-derive
its tier on the next turn. Also formats the publication spec so the
static checks pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Unset Context Meta Atomically, Keep Publication State After a Failed Write
- `saveMessage` folds the `contextMeta` unset into the same update that
persists the terminal response, on both the plain and the provenance-merge
paths, so a failure between two writes can no longer leave a completed row
carrying a disconnect snapshot's state.
- `createContextMetaPublisher` tracks whether any record has committed
separately from the retryable latest publication, so `hasPublished` stays
true after a later publication exhausts its retries and a following neutral
snapshot still overwrites the earlier record on the job.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Carry Context Meta Through the Resumable Stop Route and Neutral Resumed Completions
- The resumable Stop route copies the refreshed `jobData.contextMeta` onto the
terminal assistant message it persists, as the abort middleware already
does, so a stopped resumable response keeps the tiers that produced its
partial output.
- A resumed run that completes with neutral context state now saves
`contextMeta: null`, which unsets the paused segment's record instead of
letting an omitted field keep it on the completed row.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Trust Only Server-Authored Context Meta, Unset It on a Neutral Stop, Read It From the Job on Disconnect
- `POST /api/messages/:conversationId` and conversation imports drop any
client-supplied `contextMeta`, and the run seeds its calibration and fading
tiers only from a server-authored parent response, so a forged tier can never
shape a provider projection.
- Both Stop paths save `contextMeta: jobData.contextMeta ?? null`, so a job that
re-paused with neutral state unsets what an earlier pause stored on the row
instead of leaving it behind.
- The disconnect partial save reads the same-epoch job record for the run's
published context meta, since the client-facing resume snapshot never carries
server-private state.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧪 test: Mock the Job Store in the Resume Metadata Spec
The disconnect partial save now reads the same-epoch job record through
`GenerationJobManager.getJobStore()`, so the resume metadata spec's manager
mock provides it as the other disconnect spec already does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 🧊 fix: Keep Context Meta Off the POST Message Response
`POST /api/messages/:conversationId` returned the saved row verbatim, so a
client write against an existing server-authored message echoed the row's
stored context meta. The route now projects the row through the same client
projection the branch route uses, which drops the server-private field.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
* 📦 chore: Bump @librechat/agents to 3.7.16 for Persisted Fading Tiers
3.7.16 is the first release that carries danny-avila/agents#497, so
`Run.getFadingTier()` / `getFadingTiers()` now report the latched tier
this branch persists and `RunConfig.fadingTier` / `fadingTiers` seed the
next run from it. The dependency set is unchanged from 3.7.15; only the
package entry moves.
Adds an SDK-backed round trip to the fading spec: a first pruner latches
an informative tier, `resolveRunContextMeta` reduces it to the persisted
shape, and a second pruner seeded from that shape reproduces the first
run's projected tool-exchange bytes under instruction and calibration
drift, while an unseeded pruner under the same drift relaxes its tier and
rewrites them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF
---------
Co-authored-by: Claude <noreply@anthropic.com>
* ⚡ feat: Add Gemini 3.8 Flash Support
Adds first-class support for Google's Gemini 3.8 Flash (`gemini-3.8-flash`,
GA 2026-09-02) for both the Gemini API (AI Studio) and Google Cloud Gemini
Enterprise Agent Platform, following the Gemini 3.7 Flash integration (#14818).
The Flash-family handler that PR generalized already carries the shape 3.8
needs, so registering the model is a one-line rule rather than new behavior:
it inherits the strip of deprecated sampling params (temperature/topP/topK),
the rejected penalty params, and `thinkingBudget`, defaults to `medium`
thinking, and — like 3.7 — errors on `minimal`, so an explicit `minimal` is
substituted with the nearest supported level, `low`.
- Context window (1,048,576) in googleModels; API + cache pricing in tx.ts
- Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations
- Register the model in the Flash-family thinking-rule table
- Apply the same introductory pricing as 3.6/3.7 Flash ($0.75 in / $3.75 out /
$0.075 cached, per 1M), reverting to $1.50 / $7.50 / $0.15 on 2027-01-01;
the existing comments at both call sites now name 3.8 alongside 3.6/3.7
- Tests mirroring the 3.7 Flash coverage: thinking default, legacy-param strip,
explicit level pass-through, `minimal` substitution, versioned aliases,
context window, model-key mapping, and rates
Refs #15516
Ref: https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash
Ref: https://ai.google.dev/gemini-api/docs/pricing
* 🔧 chore: Update @librechat/agents dependency to version 3.7.15 in package-lock.json and related package.json files
* ⏭️ feat: Honor an Interrupt Before the Model Has Answered
An interrupt armed while the model is still thinking sat as "Interrupting"
until the entire turn finished, then landed as a terminal continuation. The
cause is upstream: the SDK reads `shouldPreempt` once per streamed chunk, and
its seal gate requires non-empty text — so a silent provider is never polled at
all, and a reasoning-only turn is never sealable.
`@librechat/agents` gains a discard-and-restart path for exactly that window
(`HOOK_PREEMPT_RESTART_CAPABLE`). This wires the host half:
- `GenerationJobManager.subscribePreempt` registers run-scoped wake listeners,
notified after an ACCEPTED arm — local or cross-replica, since both land in
`armPreemptIds`. Fenced on `createdAt` like every other preempt entry point,
and a throwing listener never fails the arm a queued steer depends on.
- `createSteerPreemptPoll` hands that channel to the SDK as
`StreamPreemption.subscribe`, gated on a new `isSteerPreemptRestartSupported`
probe so an SDK that cannot act on a wake is never handed one.
The wake is a hint only: `isPreemptRequested` stays the single authority, so
the level-triggered contract the seal path depends on is unchanged.
Inert until `@librechat/agents` publishes the restart contract — the probe
returns false against the pinned 3.7.11 and the run keeps today's behavior.
* 🩹 fix: Replay an Already-Armed Preempt on Subscribe
Preempt requests are level-triggered, and the window between a job becoming
steerable and the SDK installing its model-attempt listener is real. An
interrupt landing there was recorded with no callbacks to notify, and on a
silent or reasoning-only turn no later chunk poll may ever run — leaving the
request to wait out the whole turn, the exact stall this channel removes.
`subscribePreempt` now replays an existing arm to the newly registered listener
only, after registration: waking the whole set would re-notify runs that
already looked, and waking before registration would leave a concurrent arm
with nowhere to land. The `createdAt` fence is unchanged and still gates the
replay.
The SDK also reads the level flag once at each attempt's start, so this is
belt-and-braces rather than the sole cover — but it makes the host correct on
its own terms instead of resting on when the SDK happens to look.
* 🔧 chore: Update @librechat/agents dependency to version 3.7.14 in package-lock.json and related package.json files
* 🔧 chore: Update dependencies in package-lock.json and package.json
- Bump versions for @humanfs/core (0.19.1 to 0.19.2), @humanfs/node (0.16.6 to 0.16.8), @xmldom/xmldom (0.8.13 to 0.8.15), and qs (6.15.2 to 6.16.0).
- Add @humanfs/types (0.15.0) as a new dependency for @humanfs/node.
- Update dependencies for side-channel and side-channel-list to their latest versions.
* 🔧 fix: Update sanitize-html to version 2.17.7 in package.json and package-lock.json
* 🔧 chore: Update postcss-selector-parser to versions 6.1.3 and 7.1.3 in package.json
* 🤖 feat: make event actor HITL durable
* 🤖 fix: break event actor outcome cycle
* 🤖 fix: close durable actor terminal races
* fix: harden durable event actor recovery proofs
* fix: close event actor resume publication races
* feat: wire Keenable web-search provider into config, schema, and UI
Keenable landed as a search provider in @librechat/agents (#285, shipped in
3.2.58+), but LibreChat did not yet expose it. This adds the config/schema/UI
glue so it can be selected, mirroring the existing Tavily provider.
- data-provider: add `keenable` to SearchProvider type + SearchProviders enum,
keenableApiKey/keenableApiUrl schema fields, and a keenableSearchOptions block
(maxResults, site, attributionTitle, timeout).
- data-schemas: register keenable in webSearchAuth.providers and default the
key/URL placeholders in loadWebSearchConfig.
- api/web: pass keenableSearchOptions through to the provider and handle
Keenable's keyless model. Unlike other providers it authenticates with no key
(the public endpoint), picking up an optional key/URL when set; the URL
override is SSRF-preflighted like other user-provided URLs.
- client: add Keenable to the provider dropdown with an optional API-key input.
- docs: document KEENABLE_API_KEY/KEENABLE_API_URL in .env.example and a
webSearch example in librechat.example.yaml.
- tests: keyless + keyed auth resolution, config defaults, and schema parsing.
* fix: ESLint no-unused-vars and clarify Keenable yaml example
- Remove the now-unused RerankerTypes import in data-schemas web.ts (the lint
job runs with --max-warnings 0 on changed files, so this latent warning failed
CI once the file was touched).
- Note in the librechat.example.yaml Keenable stanza that a scraper (and
reranker) is still required for web search to load, and include a Firecrawl
scraper in the example.
* chore: fix import order drift (sort-imports)
* feat: add Keenable as a keyless scraper and select it without a pinned provider
The Keenable scraper landed in @librechat/agents#337, so wire the scraper
category the same way the search provider already is: `scraperProvider:
keenable` reads pages through Keenable's public fetch endpoint with no key
(a key only lifts rate limits, and the endpoint is overridden with
KEENABLE_FETCH_URL). Paired with `rerankerType: none` this makes a fully
keyless web-search stack possible for the first time.
Also closes the Codex finding on this PR: because none of Keenable's auth
fields are required, the generic auth loop skips it whenever it isn't pinned,
so a key submitted through the API-key dialog (which cannot pin a provider)
left the providers category unauthenticated. Keenable is now selected in that
case, gated on one of its values actually being present so installs that
configured nothing keep their current behavior. The scraper gets the same
fallback, additionally gated on Keenable being the resolved search provider,
so it never silently scrapes for another provider.
* fix: select the Keenable scraper from a supplied key, not only for Keenable search
The API-key dialog submits credentials and cannot pin a provider, so choosing
Keenable as the scraper while search stays on Serper/SearXNG/Tavily had no
effect: the unpinned-scraper fallback required Keenable to also be the resolved
search provider.
A supplied Keenable value now triggers it as well, which is the only signal the
dialog can send. The fallback still runs only when no keyed scraper
authenticated, and with neither trigger the category stays unauthenticated, so
a deployment that never configured Keenable is unaffected.
Note the fully keyless choice still cannot be expressed through the dialog:
Keenable's key is optional, so picking it with no key submits nothing at all.
librechat.example.yaml now documents pinning scraperProvider: keenable for that
case.
* style: Sort Keenable imports
* fix: Harden Keenable auth resolution
* fix: Preserve Keenable selection intent
* fix: Fail closed on invalid web search auth
* fix: close keenable auth gaps
* style: sort web auth imports
* fix: preserve web search selection integrity
* fix: isolate web search auth ordering
* fix: silence expected credential misses
* chore: bump agents sdk
* fix: preserve web search preference ownership
* fix: forward cleared Keenable endpoint
* style: sort web search hook imports
* fix: require intent for credential clears
---------
Co-authored-by: Ilya Bogin <ilya.bogin@keenable.ai>
* fix: upgrade redis dependencies and code to avoid elasticache bigint bug
* fix: preserve tls uri behavior with the node-redis v5 changes
* fix: satisfy node-redis v5 socket typings and clear lint in touched specs
The TLS spec passed `socket: { ca }` without `tls: true`, which node-redis
v5 accepts at runtime (the rediss:// scheme sets the flag) but its typings
reject, failing the type check. Assert the resolved socket options instead,
which covers scheme inference in both directions rather than only that the
constructor does not throw.
The benchmark spec carried two lint warnings that predate this branch and
only surface because CI lints changed files with --max-warnings=0: an unused
cache binding and a test with no assertions. Drop the binding and assert the
SCAN actually yielded keys, which is the behavior the page flattening
changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014sYJcABr6NEFmVvPxsWhfy
---------
Co-authored-by: Arnau Berenguer Jiménez <arnau.berenguer@vista.com>
Co-authored-by: NoOPeEKS <arnauapps@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
* ⬆️ chore: Bump `@librechat/agents` to v3.6.0
Bumps the pin in `api` and `packages/api` from `^3.5.1` to `^3.6.0`. The
caret on `^3.5.1` cannot cross the minor, so both manifests and the lockfile
need the explicit bump.
v3.6.0 contains three changes over v3.5.1, all additive:
- `fix: Close Subagent Child-Graph Run Steps` — subagent child graphs run via
`workflow.invoke()` outside `Run.processStream`, so the terminal sweep never
reached their steps. They now close on both the success and error paths,
which is what makes `on_run_step_closed` reliable for subagent tool cards.
- `fix: Restore Run Steps Across Process Resumes` — open run-step lifecycle
state is now persisted in LangGraph checkpoints, so a step opened by one
process closes correctly after a resume on another.
- `feat: route code execution per agent profile` — new optional
`codeSessionKey` partition for code-session ids and file refs.
No breaking changes: every new field on the public type surface is optional,
and the package's own dependency set is unchanged between the two versions
(verified against the registry), so the lockfile diff is limited to the
`@librechat/agents` entry itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5
* 🔒 chore: Sync `bun.lock` with the agents v3.6.0 bump
`bun.lock` still recorded both workspace requirements and the resolved
package as `@librechat/agents@3.5.1`, which no longer satisfies `^3.6.0`, so
`bun install --frozen-lockfile` would reject the committed state.
`bun install --lockfile-only` cannot run in this environment: bun stores no
integrity for the `xlsx` URL dependency and therefore re-fetches
`cdn.sheetjs.com`, which the sandbox network policy denies (403 on CONNECT).
The entry was updated directly instead, which is exact here because the
package's dependency graph does not move between the two versions: its
`dependencies`, `peerDependencies` and `optionalPeers` at 3.6.0 are identical
to 3.5.1 (checked against the registry), so only the version, the resolution
id and the integrity hash change. The integrity matches the one npm resolved
into `package-lock.json`, and the two existing
`@librechat/agents/*` hoisting overrides stay valid because the dependency
set they resolve is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5
---------
Co-authored-by: Claude <noreply@anthropic.com>
`@modelcontextprotocol/sdk@1.30.0` is a small maintenance release on the 1.x line
(upstream's active line is now the 2.0.0 scoped packages). The range was already
`^1.29.0`, so only the lockfile pinned the old version; the manifests move too so
the floor matches what we test against.
Nothing in it is breaking. The four changed type declarations are additive —
optional `maxBufferSize` on `StdioServerParameters`, an optional third
constructor argument on `StdioServerTransport`, optional options on `ReadBuffer`,
optional `keepAliveMs` on the server transport — and the only manifest change is
`@hono/node-server` widening to `^1.19.9 || ^2.0.5`. No new dependencies.
Two behavior changes are worth knowing about even though neither is an API break.
`ReadBuffer` now caps a single stdio message at 10 MB (previously unbounded) and
errors the transport instead of growing, which is reachable through
`StdioClientTransport` if a stdio server returns a very large single result; it
takes `maxBufferSize` if that ever needs raising. And Content-Type handling
switched from substring search to parsed media types, client and server.
Most of the release is Streamable HTTP server hardening we do not run — a 15s SSE
keep-alive, `X-Accel-Buffering: no` on SSE responses, guards so a stale stream's
cancel cannot tear down its successor, and `_closed` checks so a transport closing
mid-request stops registering streams into swept maps. None of it changes how we
behave as a client. In particular it does not address the stale-stream 409 in
#14816: that keep-alive runs in whichever server we connect to, not here.
The same substring-vs-parse mistake the SDK corrected exists in our streamable
HTTP response guard, which classified a response as SSE with
`contentType.includes('text/event-stream')`. A `Content-Type` naming the SSE type
in a parameter — `text/plain; boundary=text/event-stream` — is not an event
stream, but matched. The guard then took `canEmitFallbackSSEError`, so an
oversized body was answered with a synthetic SSE error frame the caller reads as
a well-formed response body, rather than the throw a non-SSE response gets. The
check now compares the parsed media type, via a `mediaTypeEssence` helper added
to the header utils where `mergeHeaders` already lives.
Verified against 1.30.0 rather than assuming: the package was staged into the
worktree's own `node_modules` so it shadowed the shared install, and
`packages/api` `src/mcp` ran green on it — same four pre-existing red suites as
on 1.29.0 (`MCPReinitRecovery` plus three Redis `cache_integration` suites that
need a live Redis), no new failures.
* ⚡ feat: Add Gemini 3.7 Flash Support
Adds first-class support for Google's Gemini 3.7 Flash (`gemini-3.7-flash`)
for both the Gemini API (AI Studio) and Google Cloud Gemini Enterprise Agent
Platform, following the Gemini 3.6 Flash integration (#14369).
- Context window (1,048,576) in googleModels; API + cache pricing in tx.ts.
- Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations.
- Register the model in the Flash-family handler so it inherits the existing
strip of deprecated sampling params (temperature/topP/topK), rejected
penalty params, and thinkingBudget, and defaults to `medium` thinking.
- Generalize that handler's enumerated table from a [id, level] tuple to a
rule object, so a model can also declare thinking levels it rejects. Gemini
3.7 Flash errors on `minimal` (which the Google endpoint offers in its
thinkingLevel slider), so an explicit `minimal` is substituted with the
nearest supported level, `low`. Explicit low/medium/high pass through
unchanged.
- Apply Google's introductory pricing ($0.75 in / $3.75 out / $0.075 cached,
per 1M) to Gemini 3.7 Flash and correct Gemini 3.6 Flash to the same rates.
Both revert to $1.50 / $7.50 / $0.15 on 2027-01-01; noted at both call sites.
Resolves#14802
Ref: https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash
Ref: https://ai.google.dev/gemini-api/docs/pricing
* 📝 docs: Match the House Style for Promotional Rate Comments
Align the Gemini 3.6/3.7 Flash introductory-pricing notes with the existing
Sonnet 5 convention in the same file: one comment per group, naming the models
and the exact values to restore, so the manual follow-up is unambiguous.
No rate changes.
* ⬆️ chore: Bump `@librechat/agents` to 3.4.7 for Gemini 3.7 Flash Prefill
Unblocks this PR. `NO_PREFILL_GEMINI_MODELS` is model-enumerated in the agents
SDK, so 3.4.6 does not know `gemini-3.7-flash` forbids a trailing `model`-role
turn — editing an assistant reply and resubmitting would reach Google as a
prefill and return HTTP 400 on a model this PR adds to the default list.
3.4.7 (danny-avila/agents#412, released via #413) adds it. Verified the
published tarball: `3.4.6...3.4.7` touches only
`dist/{cjs,esm}/llm/google/utils/common.*` — the prefill array and its comment.
`dist/types` is byte-identical, so there is no API surface change.
Raises the declared range in both workspaces alongside the lock. `^3.4.6`
already permitted 3.4.7, but the fix is required rather than merely compatible,
so the floor should say so.
* 📦 chore: bump `@librechat/agents` to version 3.4.2
* 📦 chore: bump `mermaid` to version 11.16.1 and update related dependencies
* 📦 chore: bump `js-yaml` to version 4.3.1 in package-lock and data-provider
* 📦 chore: bump `nanoid` to version 3.3.18 in package.json and package-lock.json across multiple packages
* 🔧 fix: Remove stray `api/tsconfig.json` breaking e2e `~` alias
An empty `api/tsconfig.json` was accidentally committed with the agents bump.
Playwright's require hook resolves path aliases from the nearest path-config,
checking `tsconfig.json` before `jsconfig.json` in each folder, so the empty
file shadowed `api/jsconfig.json` — the only place `"~/*": ["./*"]` is defined.
Every e2e spec that calls `cleanupUser` then failed on
`Cannot find module '~/cache/getLogStores'` from `api/models/index.js`.
- delete the stray file and gitignore it so tooling can't re-commit it
- register `module-alias` in `cleanupUser` so backend requires resolve
regardless of which path-config Playwright happens to find
* 📦 chore: bump `@librechat/agents` to version 3.4.3 in package.json and package-lock.json
* test: cover streamed subagent results end to end
* test: assert real e2e conversation id
* test: harden streamed subagent e2e
* test: stop incompatible subagent fixtures
* chore: update @librechat/agents to version 3.4.0 in package.json and package-lock.json
* fix(langfuse): disable central fanout media uploads
* test(langfuse): cover fanout media policy in run config
* chore(deps): bump agents for Langfuse media policy
* chore(deps): bump agents to 3.3.13
* fix(langfuse): gate central fanout media uploads
* 🛡️ fix: Run message-filter PII patterns on a linear-time regex engine
The messageFilter.pii middleware compiled admin-configured customPatterns with the native RegExp engine and ran them synchronously against every message on the shared event loop, so a catastrophic-backtracking pattern such as (a+)+$ could stall the entire process (native RegExp takes tens of seconds at roughly 32 characters) and take the instance down for every user.
Compile these patterns with RE2JS, a linear-time RE2 port with no native addon, so catastrophic backtracking is impossible regardless of the pattern rather than something the code tries to detect. Patterns using features RE2 does not support, such as backreferences, fail to compile and are dropped and logged exactly as an invalid pattern already is. The filter only tests for a match, so this is a drop-in engine swap with no behavior change for valid patterns.
* 🛡️ fix: Reject RE2-incompatible messageFilter patterns at config load
The customPatterns regex was validated with native RegExp at config load, but the runtime now compiles it with a linear-time engine (RE2) that does not support backreferences or lookaround. Such a pattern passed validation, then failed to compile and was silently dropped at request time, quietly removing PII protection after upgrade.
Reject backreferences and lookaround during config validation with an explicit message, and document RE2 syntax in the example config instead of "JavaScript-flavor". The runtime engine remains the authoritative boundary and still drops-and-logs anything this load-time check misses.
* 🧹 test: Use direct MessageFilterPiiConfig annotations in the PII specs
The added ReDoS cases satisfy the exported MessageFilterPiiConfig type directly, so the `as unknown as` assertions were unnecessary. Annotate the config objects directly, matching the repo's type-safety guidance.
* 🧹 fix: Reject named backreferences in messageFilter patterns at config load
Extend the config-load check to also reject named backreferences (\k<name>), which are valid JavaScript regex but unsupported by the linear-time runtime engine, so they surface at load rather than being dropped at request time. Together with the existing numeric-backreference and lookaround checks this covers the RE2-incompatible construct set; the runtime engine remains authoritative.
* 🛡️ fix: Preserve Unicode whitespace matching in messageFilter starter patterns
RE2's \s is ASCII-only, so after the engine swap the built-in api-key and Bearer starters no
longer matched a secret separated by non-ASCII whitespace (e.g. a non-breaking space), which
native RegExp did match. Broaden the whitespace classes to [\s\p{Zs}] so those patterns keep
their original coverage, and add a regression test for a non-breaking-space separator.
* 🛡️ fix: Validate messageFilter patterns with the RE2 engine at config load
Replace the syntax blacklist (numeric/named backreferences, lookaround) with authoritative
validation: config load now compiles each custom pattern with the same linear-time engine the
runtime uses, so any RE2-incompatible construct (including control escapes like \cA) is rejected
at load with a clear error instead of being silently dropped at request time.
The validator is swappable and defaults to native RegExp so browser builds add no engine; the
server wires the RE2-backed check at startup via configureMessageFilterRegexValidator in both
entry points.
* 🛡️ fix: Match the full whitespace set in messageFilter starter patterns
RE2's `\s` omits the vertical tab and `\p{Zs}` omits U+2028, U+2029, and
U+FEFF, so a separator built from one of those characters slipped past the
`api-key` and `Bearer` starter patterns and reached the model. Broaden the
starter whitespace class to the full JavaScript whitespace set so those
separators are covered again.
* fix: fail closed when messageFilter.pii compiles to zero patterns
DB and admin config overrides bypass the RE2 schema validation (it only
runs at YAML load), so an override whose only pattern is RE2-incompatible
was dropped at compile time, left zero patterns, and let the request
through. compile() now returns a failClosed flag when a config declared
patterns but every one failed to compile; the middleware returns 400 and
findPiiMatchInMessages returns a distinct misconfigured match that the
OpenAI and Responses controllers surface with an admin-facing message.
* 🛡️ fix: Fail closed when any messageFilter.pii custom pattern drops
compile() previously set failClosed only when every pattern dropped (patterns.length === 0 && dropped > 0). With the default starters present, a single RE2-incompatible custom override incremented dropped but left patterns.length > 0, so the filter silently enforced only the surviving subset and text matching only the dropped rule passed.
failClosed now keys off dropped > 0, so any dropped custom pattern blocks with the misconfigured 400. YAML patterns are RE2-validated at load, so dropped stays 0 for valid configs and only unvalidated DB or admin overrides can trip it. Reframed the two keeps-others-active specs to assert fail-closed and added a default-starters partial-drop regression.
* 🧹 fix: Correct the misconfigured JSDoc and drop redundant casts in the PII specs
The misconfigured flag now means any configured custom pattern failed to compile, not that every pattern failed, so its JSDoc on PiiMatch is updated to match. The partial-drop regressions now use direct MessageFilterPiiConfig annotations instead of as-unknown-as casts, keeping the specs type-checked, consistent with the rest of the suite.
* 🚦 feat: Configurable Circuit Breakers for Runaway Streamed Tool Args
* docs: forewarn create_file about the streamed tool-argument limit
The breaker failing a near-limit write should not be the model's first
exposure to the bound. Both create_file variants now state the default
64 KB per-call limit and the incremental pattern (create the first
section, extend with edit_file) in the tool description and the content
parameter description.
* fix: keep skill create_file description under the provider advisory cap
The limit-guidance paragraph pushed the skill-aware description to 1169
chars, past the 1024-char advisory bound where providers may truncate.
The skill variant now carries the guidance only in its content parameter
description, which sits closest to the generated payload and is not at
truncation risk; the shorter code-sandbox variant keeps the full
paragraph.
* 🚦 feat: per-tool streamed-arg limits with a create_file default
Thirty days of production data show create_file is the only tool class
with legitimate near-limit arguments (p99 80.6 KiB; every other tool
p99 under 10 KiB). Rather than loosening the global 64 KiB cap for all
tools, the yaml gains maxToolCallArgBytesByTool (per-tool overrides,
keyed by model-facing tool name, 0 disables that tool's guard) and
LibreChat ships { create_file: 131072 } by default; yaml entries merge
over and can replace it. Pairs with maxToolCallArgBytesByTool support
in the agents SDK and stays inert until the dependency bump.
* test: pass per-tool spec configs as plain Partial literals
The as-TAgentsEndpoint casts fail TS2352 for object-valued fields:
comparability does not grant nested literals the implicit index
signature that plain assignability does, so casts carrying
maxToolCallArgBytesByTool never sufficiently overlap. The mapper
already accepts Partial<TAgentsEndpoint>, so the new cases pass
uncast literals instead.
* chore(deps): bump @librechat/agents to 3.3.12
Activates activity-label continuity end to end. The host side landed with
the activity-groups feature (#14391) — the per-run accumulator that reads
committed headers at request-build time, the `previousLabels` payload
field, resume seeding, and the bridge passthrough — but the SDK had no
field to receive them, so the traced generation path ignored the context
and only the direct fallback rendered it. v3.3.8 carries
danny-avila/agents#356, which adds `previousLabels` to
`RunActivityLabelOptions` and renders it as the label prompt's first
section (capped at 3, each entry whitespace-collapsed and clipped at 200
chars so one malformed header cannot forge prompt sections or inflate
every later request in the run).
Effect: consecutive same-activity batches now extend the run's story
instead of restating a line already on screen, and setup batches stop
being labeled with conclusions their tools had not yet established.
Also included between v3.3.7 and v3.3.8: danny-avila/agents#354, which
anchors summary coverage to a source message id.
Lockfile carries no transitive churn — 3.3.8's dependency tree is
identical to 3.3.7's.
Verified against the installed package: 73 activityLabels tests and 218
api agents-controller tests pass, `tsc --noEmit` clean on packages/api,
and the published build renders the capped, sanitized header section
(oversized labels clipped, embedded newlines flattened to inert text).