Commit graph

5360 commits

Author SHA1 Message Date
Danny Avila
d346e5ab5e
🚰 fix: Record the Provider Drain When Shutdown Cuts a Generation Short (#15532)
* 🚰 fix: Record the Provider Drain When Shutdown Cuts a Generation Short

* fix: Await generation settlement on shutdown instead of publishing the drain

* fix: Spend the real shutdown budget and cover the initialization window

* fix: Keep the shutdown registration literal the index spec asserts on

* fix: Keep draining settlements registered while shutdown is waiting

* fix: Track provider executions at the begin/drain chokepoints instead of per caller

* fix: Open the provider tracker before the begin CAS and cap cluster workers to the primary deadline

* fix: Propagate the cluster deadline and release trackers on reconfigure and abandonment

* fix: Hold SIGTERM until the worker acknowledges the cluster deadline

* fix: Refuse provider starts after shutdown begins and harden the cluster deadline handoff

* test: Assert the multi-execution shutdown wait with both executions begun before shutdown
2026-09-03 07:11:38 -04:00
Danny Avila
f9f1b2fb95
✨ v0.8.8-rc2 (#14845)
Some checks failed
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Docker Dev Images Build / publish (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/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
* chore: prepare v0.8.8-rc2 release

* test: satisfy mongo meili static checks

* test: avoid unsafe logger spy assertion

* chore: refresh v0.8.8-rc2 release

* test: restore mongo meili test isolation

* chore: normalize v0.8.8-rc2 versions

* docs: refresh v0.8.8-rc2 release surfaces
2026-09-02 22:18:48 -04:00
Danny Avila
90cdcb384e
📦 chore: bump @librechat/agents to v3.7.17 (#15531) 2026-09-02 21:08:53 -04:00
Danny Avila
8a46ca070c
🧊 feat: Persist Context Fading Tier Across Agent Runs (#15505)
* 🧊 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>
2026-09-02 20:29:43 -04:00
Danny Avila
676cd512b0
🫙 fix: Drop Blank Content Blocks From Promptless Sends (#15530)
* 🫙 fix: Drop Blank Content Blocks From Promptless Sends

* fix: Preserve the promptless turn instead of dropping it
2026-09-02 20:15:00 -04:00
Danny Avila
790f2b3153
⚡ feat: Add Gemini 3.8 Flash Support (#15519)
Some checks failed
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* ⚡ 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
2026-09-02 15:06:40 -04:00
Danny Avila
29f738dea8
⏭️ feat: Honor an Interrupt Before the Model Has Answered (#15491)
* ⏭️ 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.
2026-09-02 13:35:52 -04:00
Danny Avila
0219513115
🛑 fix: Forward Run Abort Signal to Foreground Tool Calls (#15517)
* 🛑 fix: Forward Run Abort Signal to Foreground Tool Calls

Stopping a generation left in-flight tool calls running on the far side.
For MCP over streamable-http the external server kept burning CPU until
the tool finished or timed out, with no `notifications/cancelled` ever
sent.

The MCP layer was never the problem: `MCPManager.callTool` already
spreads `options.signal` into `client.request`, and the SDK's
`Protocol.request` sends `notifications/cancelled` the moment that signal
aborts. The signal simply never arrived.

Agent tools do not execute inside LangGraph's ToolNode — `eventDrivenMode`
dispatches `on_tool_execute` and `createToolExecuteHandler` invokes them.
The agents SDK puts the run's abort signal on the batch request
(`ToolExecuteBatchRequest.signal`, documented as one handlers should
forward), but the handler destructured every field except `signal` and
built its invoke config without it. `config.signal` was therefore
`undefined` in every foreground tool, so `createMCPTool` derived no
signal and the SDK registered no abort listener.

Forward it. This restores cancellation for all signal-aware foreground
tools, not just MCP. The detached background invoke keeps its own
controller, since that work deliberately outlives the turn.

An aborted call now rejects promptly and resolves an error result rather
than hanging, so the abort is logged at debug instead of as a tool error
— a user pressing Stop should not spray the error log.

* 🔇 fix: Keep Cancelled Tool Calls on the Filtered Path and Out of Error Logs

Addresses both findings from the Codex review of 48fded564a.

P1 — the cancellation branch returned before `filteredToolOutputResult`,
so with tool-output filtering configured an aborted call's error text
reached the turn uninspected. Worse, `runSignal.aborted` says the run is
over, not that this rejection was the cancellation: an unrelated failure
racing the Stop took the same unfiltered exit. Cancellation now only
selects the log level; filtering and the result shape are unchanged.

P2 — `createMCPTool` logs its own error before wrapping and rethrowing,
so every Stop during an MCP call still emitted an error-level MCP entry
and could pollute operational alerts. That catch now recognizes the abort
and logs at debug; the wrapped message still reaches the turn.

Both are covered by tests that fail against the previous commit.

* 🎯 fix: Require an Abort-Shaped Error and Spare Shared OAuth Flows

Addresses both findings from the Codex review of b3c67082cd.

An aborted run signal proves the turn is over, not that the rejection in
hand was the cancellation — a permission, OAuth, or upstream failure can
reject in the same tick a user presses Stop. Both quiet-log branches now
require the error to look like an abort as well, so a real failure racing
the Stop stays at error level and visible to operational alerts.
`isAbortError` moves to `@librechat/api` and the copy in
`abortMiddleware` is retired, keeping one implementation. Its spec pulls
the real function through its partial package mock rather than asserting
a stub.

Forwarding the run signal also reached ActionService, where two of the
three OAuth flows are keyed `userId:action_id` and so outlive the run
that opened them: a second run for the same action joins the very same
record, and the browser's OAuth callback reads its metadata to exchange
the code. `monitorFlow` deletes the key when a waiter's signal aborts, so
one Stop could strand a concurrent run and discard an authorization the
user had already granted. Those two flows no longer take the signal; the
run-scoped login flow, keyed by thread and run, still does.

Every guard is covered by a test that fails when the guard is removed.

* 🧷 fix: Detach the Stopped OAuth Waiter Instead of Dropping Its Signal

Addresses the Codex review of d53ff170ab.

Withholding the run signal from the shared Action OAuth flows protected
concurrent runs and the browser callback, but left this invocation
attached to the flow after the Stop. A callback landing late then resumed
`_call` straight into `preparedExecutor.execute` — a consequential API
request running after the user had already stopped the turn. Handing the
signal back is not the answer either, since `monitorFlow` deletes the
shared `userId:action_id` key on abort.

`detachOnAbort` separates the two: the caller stops waiting the moment
the signal aborts, while the shared flow runs on for whoever else needs
it — another run, or the callback exchanging its code. Late settlement of
the detached work is swallowed rather than surfacing as an unhandled
rejection.

Both shared flows now use it. The run-scoped login flow keeps the signal
directly, as nothing outside its run observes it.

* 🪪 fix: Keep an Action OAuth Abort an Abort

Addresses two of the three findings from the Codex review of 70fc0782b1.

Detaching the stopped waiter left the rejection to be misread on the way
out. The refresh catch treated it as a failed refresh and called
`requestLogin()`, emitting an OAuth prompt and opening pending
authorization state for a turn that had already ended. `requestLogin` and
the surrounding auth catch relabelled it `Failed to authenticate OAuth
tool` / `Authentication failed`, and the outer catch handed that to
`logAxiosError` — so an ordinary Stop produced two error-level entries
and returned failure text as the tool's result. Each boundary now lets an
abort through unchanged, and the outer catch logs it at debug and
rethrows so it stays a cancellation end to end.

Deferred: waiter-only cancellation in `FlowStateManager`, so a detached
`monitorFlow` stops polling instead of running out the flow's TTL. That
poller is bounded at three minutes and lives exactly as long as it does
for a waiter that never left, so it changes no lifetime this branch
introduced; giving `FlowStateManager` a non-destructive abort mode is its
own change, shared with MCP and indexSync.

* 🤫 fix: Classify the Abort at the MCPManager Boundary Too

Addresses the Codex review of 9bcf1aa57e.

`MCPManager.callTool` logs every rejection at error level before
rethrowing to `createMCPTool`, so a user Stop still emitted an MCP error
from the inner boundary even after the outer one learned to recognize it.
The same signal-plus-abort-shape classification now applies here.

The review also noted, correctly, that the existing test could not have
caught this: it replaces `getMCPManager` wholesale, so it only ever
exercised the outer logger. The new cases drive the real manager with a
connection whose `client.request` rejects the way the SDK does once a
request signal aborts, and cover both the cancellation and a genuine
failure racing the Stop.

* 🧹 chore: Restore Import Order in the Agents Tool Handler
2026-09-02 13:32:33 -04:00
Dustin Healy
0b96fb9f70
🪪 feat: Propagate Tenant to Agent Model Headers (#15521)
* fix: propagate tenant to agent model headers

* fix: propagate tenant across agent model calls
2026-09-02 13:25:38 -04:00
Danny Avila
d5b2a8580e
📖 feat: Add Claude Fable 5.1 Support (#15512)
Some checks are pending
Docker Dev Images Build / publish (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Claude Fable 5.1 (`claude-fable-5-1`, GA 2026-09-01) succeeds Claude Fable 5
at the same input/output prices, with a 1M context window and 128K max output.
The Mythos-class helpers added for Fable 5 already match it — `isMythosClassModel`
tests `claude-(?:fable|mythos)[-.]?\d`, so adaptive thinking, summarized-display
opt-in, sampling omission, 1M context, the 128K output clamp, prompt caching,
the Vertex multi-region gate, and the Bedrock PDF exemption all cover 5.1
without a code change. What was missing was registration and pricing.

Cache reads are the one rate that differs, and the substring matcher would have
silently gotten it wrong: `claude-fable-5-1` falls through to the `claude-fable-5`
key, which prices cache hits at the usual 0.1x base input ($1/MTok). Fable 5.1
and Mythos 5.1 price them at 0.025x ($0.25/MTok), so every cache read would have
billed 4x — on exactly the long agentic sessions this model is built for.

- Register `claude-fable-5-1` in the shared Anthropic list and
  `global.anthropic.claude-fable-5-1` in the Bedrock list
- Add 1M context / 128K output entries for `claude-fable-5-1` and
  `claude-mythos-5-1`
- Add $10/$50 rates and `{ write: 12.5, read: 0.25 }` cache rates for both
- Update the `.env.example` and Vertex `librechat.example.yaml` examples
- Test that 5.1 resolves to its own key rather than collapsing onto 5.0, across
  token maps, pricing, cache rates, and Mythos-class detection

The three documented breaking changes from Fable 5 need no handling here:
LibreChat never sends a forced `tool_choice` on the Anthropic path (the agents
SDK omits the param when unset, so the API default `auto` applies); thinking
blocks an earlier model can't read are dropped server-side, unbilled; and
history is rebuilt from stored content parts rather than replayed signed
thinking blocks, so the prefix-binding check has nothing to reject.

Refs #15506
2026-09-02 10:45:54 -04:00
Danny Avila
1c7a172321
🕹️ feat: Configure BYOM Tool Permissions (#15504)
* feat: add configurable BYOM permissions

* test: cover BYOM permission settings live

* fix: harden BYOM permission settings

* perf: reuse principals for environment discovery

* chore: normalize BYOM settings style

* fix: Align BYOM Discovery and Admission

* fix: Harden BYOM Permission Boundaries

* fix: Align BYOM Skill Approval Routing

* fix: Preserve Lazy BYOM Approval Capabilities

* perf: Parallelize Cached Environment Reads
2026-09-02 10:38:00 -04:00
Danny Avila
0cb1118e08
📱 test: Add Mobile Chat Performance Benchmark (#15510)
* test: add mobile chat performance benchmark

* fix: isolate mobile benchmark measurements
2026-09-02 10:36:33 -04:00
Danny Avila
057039570b
🔭 feat: Audit PII Filter Findings (#15508)
* feat: add audit-only content filter action

* refactor: align filter action typing and tests

* fix: preserve audit-only filter behavior

* fix: enforce audit mode across content paths

* fix: honor audit mode for canonical files
2026-09-02 10:36:06 -04:00
Danny Avila
a20048ab2d
🫱 fix: Keep Polling Through Queued Run Handoffs (#15509)
Queue a message during a run — queue, not steer — and the row is server-owned:
`useQueueDrain` deliberately declines the boundary (`serverOwnsBoundary`) and
the backend admits the successor itself. So the client has to hear about that
run through the active-job list, exactly like a background-tool continuation.

It cannot. Finishing a run empties the list — the client removes its own job
optimistically the moment FINAL lands — and the poll is disabled on an empty
list, so it goes quiet at precisely the moment the successor is admitted. With
the tab already focused there is no focus refetch either, which is why this
reproduces on desktop and not only on mobile. The announcement never arrives,
so the re-arm never runs and the pane sits on the previous turn until a reload.

Keep asking for a bounded window after the last known job ends. The window is
module-scoped rather than per-observer because this is one shared query and a
component mounting mid-handover should inherit the grace, not restart it.

This is the other half of the previous commits: without the re-arm the
announcement lands on a pane that still reads its finished submission as an
attachment, and without this the announcement never lands at all. Neither half
is observable alone.
2026-09-02 09:38:42 -04:00
Danny Avila
519e64769a
🚢 fix: Reconnect Keyv Redis After Replica Failover (#15502)
* 🔁 fix: Recover Keyv Redis Client From READONLY Replica After Failover

When a Sentinel failover demotes the master a pod is connected to, the
ioredis client recovers through `reconnectOnError`, but the Keyv/node-redis
client never does: `socket.reconnectStrategy` only runs when the socket
closes, and a demoted replica keeps existing sockets open while rejecting
every write with `READONLY`. Every cache write through the Keyv client
(USER_PRINCIPALS, MCP catalog, server configs) then fails until restart.

- Add a READONLY recovery hook for the standalone node-redis client that
  tears the socket down and reconnects, re-resolving to the current master
- Debounce attempts with `REDIS_READONLY_RECOVERY_INTERVAL` (5s default)
  and retry on the next error, since a reconnect issued before the
  topology settles can land on the demoted node again
- Route all three surfaces where READONLY replies appear through the hook:
  the client error event, the Keyv cache error funnel, and Lua scripts
  evaluated via `evalKeyvRedisScript`, which reject straight to callers
- Skip cluster clients, which follow MOVED to the owning master already
- Cover the hook with a fake RESP server so real node-redis and Keyv code
  paths are exercised against a node whose role flips mid-connection

Fixes #15500

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🛟 fix: Harden READONLY Recovery Retry, Clear Path, and Clock

Address review findings on the READONLY failover recovery:

- Retry on the next error of any kind after a failed reconnect. A failed
  `connect()` leaves the client closed, so later failures surface as
  ClientClosedError rather than READONLY and the hook never fired again
- Route the namespace `clear()` override through recovery. It deletes
  through the raw client, bypassing the Keyv error funnel, so a READONLY
  rejection there previously left the pod stranded
- Debounce on an injectable monotonic clock (`performance.now`) instead of
  `Date.now`, so wall-clock steps cannot suppress or duplicate attempts;
  the interval test drives the clock instead of sleeping
- Reuse `isReadonlyReplicaError` in the ioredis `reconnectOnError` hook so
  READONLY detection has one definition
- Document the in-flight command rejection that a socket teardown implies

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🛟 fix: Gate READONLY Recovery on Client State

Address the second review pass on the READONLY failover recovery:

- Act only on a client the hook can reason about: a READONLY reply on a
  ready socket, or a client its own failed reconnect left closed. While
  node-redis runs its own reconnect loop (open but not ready) the hook
  stays out, since a second `connect()` would start a concurrent loop
  that leaks sockets into one shared reply decoder
- Clear the pending retry whenever an error arrives on a ready client, so
  a client that Keyv auto-connected after a failed attempt is never torn
  down by a later unrelated error
- Drop the client error-event wiring: node-redis rejects command error
  replies straight to the command promise and emits `error` only from its
  socket and connect loop, so that listener could never see a READONLY
  reply and only fed connect-loop errors back into the hook
- Cover the connect-loop, self-recovered, and Keyv auto-connect outage
  cases, and a rejected namespace clear, with a SCAN-aware RESP server
- Add the new export to the `redisClients` mocks in the cacheFactory specs
2026-09-02 08:57:40 -04:00
Danny Avila
b1c56239f9
🧤 feat: Enforce Safe BYOM Tool Approvals (#15503)
* feat: add safe default approvals for BYOM code

* fix: preserve safe BYOM approval baseline
2026-09-02 08:53:16 -04:00
Danny Avila
434fa377c0
🗜 test: Clamp Recursive Child Inputs to Builder Types (#15498)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (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
Docker Dev Images Build / publish (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
2026-09-01 22:23:57 -04:00
Danny Avila
29e9d08163
🪛 chore: Align Vite Build Config With Native ESM (#15495)
* fix: Resolve Vite build warnings

* style: Sort token usage transition classes
2026-09-01 22:22:28 -04:00
Danny Avila
6372cb08ee
🧶 fix: Untangle Parallel Subagent Descriptor Paths (#15492)
* fix: isolate parallel subagent descriptor walks

* fix: preserve canonical subagent config identity

* fix: separate subagent graph assembly from path validation
2026-09-01 21:48:15 -04:00
Danny Avila
6d56d3b6fe
🧴 chore: Refresh Sanitizer and Selector Parser Dependencies (#15494)
* 🔧 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
2026-09-01 21:38:08 -04:00
Marco Beretta
2cf1007f3c
🧺 feat: Preserve Partial Work at Tool Call Limits (#15486)
* feat: Handle reaching the agent tool-call limit gracefully

When the graph exhausts its per-turn step budget, persist the turn as
unfinished with finish_reason tool_call_limit instead of a red error.
Warn the model as the budget runs low, and offer Keep going or Answer now.

* docs: Add screenshots of the tool-call-limit notice

Light and dark captures of the unfinished-turn card for the PR description.

* style: Tighten the tool-call-limit notice card

Pin dismiss to the top-right with uniform rounding, use a slightly
smaller hit target, and round the card to xl. Refresh the PR screenshots.

* fix: Address Copilot review findings on the step-budget path

Skip empty and subagent-scoped PostToolBatch events when counting
rounds. Drop the double-cast in the notice test helper and coerce the
tool-call-limit finish reason in the Continue-gate spec.

* fix: Address Codex review findings on budget and dismiss

Pass the resolved recursion limit into the OpenAI-compatible
processStream path so the step-budget hook matches the graph.
Reset the notice dismiss state when the displayed message id changes.

* fix: Unblock CI typecheck and import sorting

Declare recursion_limit on the OpenAI service agent type, sort that
file's local imports, and route the notice test context through unknown
so tsc accepts the partial ChatContext fixture.

* fix: Sort remaining PR imports for static checks

* chore: Remove tool-call-limit notice screenshots

* fix: Keep recovery actions from consuming composer drafts

Pass empty file, skill, and quote overrides on Keep going and Answer
now so a staged compose is not attached or drained.
2026-09-01 21:38:02 -04:00
Danny Avila
71544f8236
🗿 chore: Upgrade gRPC to Prevent Receive Buffer Exhaustion (#15493) 2026-09-01 21:32:20 -04:00
csdivad
8a40e9f675
🔄 fix: Reindex MeiliSearch Documents After Indexed-Projection Changes (#15484)
* 🔄 fix: Reindex MeiliSearch Documents After Indexed-Projection Changes

- Introduce _meiliIndexSchemaVersion schema field and MEILI_INDEX_SCHEMA_VERSION constant
- Stamp schema version on single-document and batch index writes
- Query unindexed and stale-version documents in syncWithMeili
- Count stale documents in pendingIndexing in getSyncProgress
- Add spec coverage for reindexing stale-version documents

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* 🔄 test: use the registered Meili conversation model

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* 🔄 style: satisfy Prettier in Meili indexing progress counts

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-01 21:19:22 -04:00
csdivad
ce704f5c11
🔍 fix: Include Message-Matched Conversations in Sidebar Search (#15483)
* 🔍 fix: Include Message-Matched Conversations in Sidebar Search

- Union conversation and message index matches in getConvosByCursor
- Type searchMessages with SearchParams and SchemaWithMeiliMethods
- Inject searchMessages into createConversationMethods
- Bound sidebar Meili queries with 1000 limit and attributesToRetrieve
- Fall back to title-only matches when message search fails or is uninjected
- Escape filter user values in search queries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(search): preserve original conversation keys in sidebar hits

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* 🔍 fix: reuse the loaded conversation model

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* 🔍 fix: type the loaded conversation search model

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-01 21:17:11 -04:00
Danny Avila
164f146bdf
🛞 feat: Manage Personal Code Environment Lifecycles (#15369)
* feat: add self-service code worker pairing

* fix: enforce code worker principal boundaries

* fix: separate pairing control planes from execution

* fix: preserve principal worker routing

* fix: pin artifact traffic to code workers

* fix: authenticate code worker selection

* test: mock worker-aware artifact headers

* fix: harden self-service code worker routing

* fix: fail closed for principal code pairing

* feat: manage personal code environments

* fix: harden personal code environment lifecycle

* fix: satisfy backend lifecycle types

* fix: harden principal environment lifecycle and quotas

* fix: fence environment lifecycle races

* fix: reconcile code environment stack

* style: format code execution headers

* fix: reconcile code environment lifecycle

* fix: close self-service pairing gaps

* fix: close code environment reconciliation gaps

* fix: close code environment UI review findings

* fix: satisfy code environment type checks

* fix: route code priming to selected worker

* fix: harden code environment lifecycle fences

* fix: type lease renewal interval

* fix: harden environment lifecycle recovery

* fix: close lifecycle recovery races

* fix: type compensated agent timestamps

* fix: fence lifecycle cleanup by tenant and revision

* fix: scope environment revision fencing
2026-09-01 21:10:05 -04:00
Abhishek Biswas
38183fdc13
🧳 fix: Carry Custom Endpoint Names Through Chat URLs (#15424)
`createPayload` interpolated the endpoint name straight into the chat URL,
so a custom endpoint named `Company/API` produced
`/api/agents/chat/Company/API`. The slash reads as a path separator, the
request no longer matches the router's `/:endpoint`, and it falls through
to the catch-all handler, which answers `404 Endpoint not found`.

Encoding the name keeps it a single path segment. Express matches the
encoded segment and decodes it back, and the endpoint the server acts on
comes from the request body regardless, so nothing else has to change.

Closes #15270
2026-09-01 21:07:29 -04:00
csdivad
0f9667b391
🔎 fix: Respect Search Page Limits in MeiliSearch Queries (#15482)
* fix(api): align message search with page size

Pass a positive, capped page size to MeiliSearch and use the same conversation limit so requested message search pages are not silently truncated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(data-schemas): expand shared-link search results

Request the MeiliSearch result ceiling using only conversationId so cursor pagination is not limited to the default 20 hits or burdened with unused fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-01 21:05:25 -04:00
Danny Avila
a3c2277c38
🚧 fix: Narrow Chat MCP Selections to What the Picker May Offer (#15478)
* 🚧 fix: Narrow Chat MCP Selections to What the Picker May Offer

The chat MCP picker offers a server only when the operator has not hidden it
(`chatMenu: false`) and the user reaches it directly rather than through an
agent (`consumeOnly`). Nothing enforced either condition on the request, so a
stale client, a replayed body, or a hand-written one could name any server and
get its tools — `consumeOnly` in particular is a permission the registry derives
per user, so ignoring it let a user reach in plain chat a server they had only
been granted through an agent.

Narrow the picker's selection in the two ephemeral loaders, against the same
predicate the client's `selectableServers` uses. The resolver is injected, so
`packages/api` stays free of the registry and the loaders keep their existing
`deps` shape.

Provenance is why this lives in the loaders rather than at the tool-resolution
chokepoint: only here is a server name known to have come from the picker.
Servers a model spec pins are the operator's own choice and are added after the
narrowing, so pinning a chat-hidden server to a spec keeps working.

`chatMenu` only ever comes from the config tier, so the overlay settles it with
no lookup at all. Lookups for the rest run concurrently and are cached per
server and user. A name the registry cannot resolve — request-tier, declared on
the body and never registered — is kept, as is one whose lookup fails: this
narrows an already-authenticated selection and is not the authorization
boundary.

* 📝 docs: Say What chatMenu Now Bounds

The schema still described `chatMenu` as dropdown visibility alone, which is
misleading now that it also bars a server from the chat selection a request
carries. Spell out both halves, and what it deliberately does not restrict:
agents, and servers a model spec pins.

* 🛡️ fix: Close Four Gaps in the Chat MCP Narrowing

Dedupe before resolving. The selection is request-supplied and the loaders used
to build a `Set` immediately; mapping the raw array first let one body repeat a
name enough times to launch a resolver — registry, cache, and ACL reads, with no
single-flight — per occurrence.

Resolve names through the tool-loading aliases. A configured `private server` is
addressable as `private_server` downstream, so an exact-key lookup let the
normalized spelling walk past a `chatMenu: false` check and still load tools.
Names are returned as sent; only the decision is canonicalized.

Let the request overlay settle `chatMenu`. The config tier is where the flag
lives and is what the picker itself was built from, so an overlay that re-enables
a server now outranks the durable config the registry returns, which was silently
dropping a valid selection. `consumeOnly` still comes from the registry.

Narrow the request body, not just the loader's copy. `applyContextToAgent` reads
`req.body.ephemeralAgent.mcp` directly and prefers it over the agent's tools, so
a hidden server's `serverInstructions` reached the model context even after its
tools were dropped.

* 🧭 fix: Ask the Registry Which MCP Servers the Picker May Offer

Three rounds of review found the same mistake in three disguises: this narrowing
re-derived the registry's tier precedence instead of asking for it. A config
entry was treated as authoritative over a user-DB or process-backed server of the
same name; the alias map was built from config names only, so a DB server named
`private_server` was misrouted to a config `private server`; and the overlay
short-circuit ran before any of that could be consulted.

Resolve the accessible set through `resolveAllMcpConfigs` instead — the same call
behind `GET /api/mcp/servers`, so what the request may use and what the picker
was offered come from one place and cannot drift. Precedence, cross-tier name
collisions, and the config overlay are the registry's job again, and the
selectability test is a single flag check over what it returns.

The result is smaller: no overlay branch, no per-server lookups, one catalog read
regardless of how long the selection is, and aliases built over the whole
accessible set so an exact name always outranks another server's normalized form.

Publish the servers actually in play back onto the request body, spec pins
included. `applyContextToAgent` prefers that list over the agent's tools, so
narrowing to the picker selection alone had started dropping a spec-pinned
server's `serverInstructions` while suppressing a hidden one's.
2026-09-01 18:47:36 -04:00
Danny Avila
51edb2a660
🛫 feat: Enable Self-Service Code Worker Pairing (#15367)
* feat: add self-service code worker pairing

* fix: enforce code worker principal boundaries

* fix: separate pairing control planes from execution

* fix: preserve principal worker routing

* fix: pin artifact traffic to code workers

* fix: authenticate code worker selection

* test: mock worker-aware artifact headers

* fix: harden self-service code worker routing

* fix: fail closed for principal code pairing

* style: format code execution headers

* fix: close self-service pairing gaps

* fix: satisfy code environment type checks
2026-09-01 18:46:33 -04:00
Danny Avila
f72a5b33c3
🛞 fix: Roll Overflow Tenant Labels Into Aggregate Metrics (#15474) 2026-09-01 18:45:20 -04:00
Oliver Faust
5e49868f0a
🔑 fix: Decode Action OAuth Credentials After Decryption (#14698)
* 🔑 fix: Decode Action OAuth Credentials After Decryption

Action client credentials are URL-encoded before encryption, but the OAuth
token exchange decrypted them with `decryptV2` alone, sending values like
`abc%40domain.com` to the provider and causing 401s for secrets containing
`@`, `+`, `=`, `/` or `:`. Decoding falls back to the raw decrypted value so
legacy secrets stored before 299cabd6e (which contain an unescaped `%`) still work.

Fixes #14636

* 🔑 fix: Share Action Credential Codec Between Read Paths

The encode-before-encrypt convention had two readers with different
behaviour. `ActionService.decryptSensitiveValue` decoded unconditionally,
so a credential stored before encoding was introduced (299cabd6e, March
2025) containing a stray `%` threw `URIError` and failed every read of the
action, not just the token exchange.

Extract the pair into `packages/api/src/actions/crypto.ts` as the single
implementation and consume it from both `ActionService` and the OAuth token
exchange, so the two paths cannot drift again.

Tests cover the codec against real crypto: reserved characters and literal
percent escapes round-trip, pre-encoding values are returned unchanged, and
a pre-encoding value with a stray `%` no longer throws.

A version marker on newly encrypted values was prototyped and dropped: it
changes the stored format for every action credential while fixing nothing,
since values written by the encoding convention already decode correctly
and pre-encoding values stay unmarked either way.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-01 18:42:28 -04:00
Danny Avila
aa7539909b
🪶 feat: Enable Standalone Skill Authoring (#15480)
* feat: enable standalone skill authoring

* test: align skill e2e flow with enable toggle

* fix: preserve legacy skill state on version restore
2026-09-01 18:39:05 -04:00
Danny Avila
dfb264811c
🪢 test: Prove MCP OAuth Resume End to End (#15485) 2026-09-01 18:38:39 -04:00
Danny Avila
5b75c79831
🫥 fix: Clear Hidden MCP Chat Selections (#14159)
* 🫥 fix: Clear Hidden MCP Chat Selections

A server hidden with `chatMenu: false` stayed active in chat when it had been
selected while still visible. The selection persists to localStorage, and the
guard that clears stale selections tested the *filtered* server list — which is
empty precisely when every configured server is hidden, so the guard could never
fire and the selection survived every reload.

Gate the prune on the unfiltered catalog instead. It is non-empty in exactly the
case the old guard missed, and it stays a guard rather than a load flag so an
empty catalog — still loading, or a degraded read — is never mistaken for "the
admin removed everything" and used to wipe a selection irrecoverably.

Clear the ephemeral agent alongside the atom: it is what carries the selection to
the server, so a hidden name has to leave it too, not just the dropdown's state.

Also label the dropdown badge from the servers the menu actually offers. It
counted the raw selection, so a hidden server rendered as "Server Four" or
"2 selected" above an empty menu — the symptom that reads as a server you cannot
turn off.

* 🔧 fix: Keep Spec-Assigned MCP Servers Out of the Hidden-Server Prune

A model spec may deliberately assign a server the chat menu hides, and
`applyModelSpecEphemeralAgent` seeds it straight into `ephemeralAgent.mcp`. The
prune could not tell that apart from a stale selection, so activating such a spec
dropped its server — and because the same function re-reads `LAST_MCP_` for
existing conversations, the loss persisted across navigation.

Exempt whatever the active spec pins. `chatMenu` hides a picker entry; it does
not override an admin's spec assignment.

* 🔒 fix: Confine the MCP Selection Prune to the Hook Instance That Owns It

`useMCPServerManager` has seven call sites, and only `BadgeRowContext` reads the
chat selection — the rest mount it for the catalog, the server actions, or the
status icons. Every instance keyed to the same conversation shares one atom and
one ephemeral agent, so those six were all running the prune too, and the
always-mounted `useSideNavLinks` has no spec context to exempt from: it would
rewrite the selection the picker's own instance was deliberately keeping.

Make managing the selection opt-in, off by default at both layers, and let the
badge row alone opt in. The six catalog consumers stop touching chat selection
state entirely, which is what they should have been doing all along.

Tests now state whether they own the selection rather than inheriting it from a
default, so each one exercises the same configuration as the instance it models.

* 🔁 fix: Mirror the MCP Selection Everywhere, Prune It Only in the Owner

Gating the whole ephemeral-agent sync on selection ownership was too broad. The
action paths — `initializeServer`'s OAuth success, the revoke handler — build
their next selection from `mcpValues` and write it back through `setMCPValues`,
which also rewrites the shared ephemeral agent. An unmirrored instance therefore
wrote a stale list: authenticating server B could drop an existing selection of
server A, leaving only B.

Mirroring and pruning are separate concerns. Every instance mirrors, so each one
builds on the current selection; only the owner prunes, because only it knows
which servers a spec pins.
2026-09-01 18:38:14 -04:00
Danny Avila
6195273429
🧲 fix: Anchor Memory Partitions to Accessible Agents (#15475)
* fix: validate agent memory partitions

* fix: authorize agent memory mutations

* test: cover agent manager memory access

* fix: centralize agent memory authorization

* fix: harden agent memory authorization

* fix: support synchronous role providers
2026-09-01 18:37:52 -04:00
Danny Avila
dccd7ae496
🧶 fix: Preserve Multi-Agent Tool Call History (#15468)
* 🧭 fix: Preserve Multi-Agent Tool History

* 🧰 fix: Preserve Lazy Subagent Tool History

* 🧪 test: Enable Lazy Code History Fixture

* 🧭 fix: Derive Effective Lazy Tool History

* 🧪 fix: Tolerate Empty MCP Name Audits

* 🧪 test: Mock Lazy MCP Name Audit

* 🧭 fix: Expand Lazy Historical Tool Names

* 🧱 fix: Derive Durable Wildcard History

* 🧬 fix: Traverse Nested Wildcard History

* 🔭 fix: Resolve Wildcard Server Boundaries

* 🔐 fix: Persist MCP Call Identity

* 🪪 fix: Stamp Resolved MCP Execution Identity

* 🕸️ fix: Resolve Graph Member MCP Identity

* 🧭 fix: Fall Back From Empty Graph Member ID

* 🧭 fix: Preserve Aggregated MCP Identity

* 🧭 fix: Harden MCP Identity Resolution

* 🧭 test: Assert Concurrent Graph Limit Invariant

* 🧭 fix: Centralize Lazy History Policy

* 📦 chore: Bump `@librechat/agents` to v3.7.13

* 🧪 test: Align Content Filter Fixture With SDK History
2026-09-01 18:37:45 -04:00
Danny Avila
a290677d21
🗿 fix: Bound Background Task Retention (#15477)
* fix: bound background task registry memory

* fix: preserve background registry capacity state

* fix: align background payload accounting

* fix: harden background capacity admission

* fix: honor background payload admission

* fix: make background eviction atomic
2026-09-01 16:55:00 -04:00
Danny Avila
ce209102b8
🧫 fix: Contain Detached Tool Output (#15476) 2026-09-01 16:54:41 -04:00
Danny Avila
0f585497ea
🚮 fix: Replace Backtracking LangChain Error Regexes (#15473) 2026-09-01 16:52:48 -04:00
Danny Avila
6122d210da
🛄 fix: Reclaim Pending MCP OAuth Prompts on Resume (#15471) 2026-09-01 16:52:18 -04:00
Danny Avila
74535662fe
🎖️ fix: Apply Fallback Role on Group Overage (#15472) 2026-09-01 16:44:24 -04:00
Danny Avila
67e2107d74
🔐 fix: Keep OpenID Metadata Out of Logs and Traces (#15470)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
* 🔒 fix: Keep Secrets Out of Debug Logs and Spans

* 🧯 fix: Derive Safe Exception Types

* 🛡️ fix: Close Diagnostic Redaction Gaps

* 🧼 fix: Redact Escaped JSON Secrets
2026-09-01 13:22:12 -04:00
Danny Avila
d83958d3b3
📎 fix: Keep SharePoint Picker Selections While Navigating (#15389)
The picker never set `selection.enablePersistence`, so it dropped the current
selection every time the user opened another folder or switched pivot. A selection
could only ever be assembled from a single location — pick three files, open another
folder, and the three are gone.

One option on the picker configuration; no change to what LibreChat does with the
result.

Fixes the selection half of #15237. Folder selection — the other half — is a new
capability rather than a fix, and is parked on `feat/sharepoint-folder-selection`.
2026-09-01 12:59:50 -04:00
Danny Avila
537ddba6bb
🫴 fix: Hand Composer Focus Back After Upload Dialogs (#15469)
Typing a message, pasting an image, then pressing Enter did nothing: the
composer held the full draft, the send button was enabled, and Enter was
inert until the user clicked back into the textarea — which reads as
"the textarea needs a text change before it will submit".

The send gating was never at fault. Radix's modal `DialogContent` always
cancels its default close-focus restore and focuses
`context.triggerRef.current` instead. `DragDropModal` — the "Select Upload
Type" dialog a paste or a drop opens — is opened programmatically and so has
no `Dialog.Trigger`, leaving that ref null: nothing is focused and focus
falls to `document.body` when the dialog unmounts. Enter-to-send is a
keydown handler on the textarea, so from `body` it is never reached.

`UploadModalProvider` now records the element the upload was started from,
captured in `openModal` before the dialog mounts (an effect runs too late —
Radix has already moved focus), and restores it once the dialog closes,
queued behind Radix's own deferred restore. This covers every close path:
picking a destination, Cancel, Escape, and dismissing the overlay.

`OGDialog`'s existing `triggerRef` seam does not reach this case, since it
only fires through Radix's `onOpenChange` and choosing an upload destination
calls `closeModal()` directly.
2026-09-01 12:56:03 -04:00
Danny Avila
c3beae1e18
🔌 refactor: Give the Subagent Surface a Host Seam and Its Own Jotai State (#15455)
* 📐 docs: Record Who Owns Which Piece of Client State

* 🔌 refactor: Give the Subagent Surface a Host Seam and Its Own Jotai State

The subagent chat surface reached up into seven application atoms and could
not be lifted anywhere, and the six atoms it owns were still Recoil while its
sibling stores had already moved to Jotai.

Feature-owned state converts to Jotai and stays where it is: the selection,
per-task control receipts, live progress and the parent-stream fence. The
control atom keeps its session-storage retry — restored per store rather than
per family member, so a reload still sees what the last one persisted.

App-global state now arrives through one `ChatSurface` value the host supplies:
`enterToSend`, `maximizeChatSpace`, `showScrollButton`, the reader's composer
bindings, a `claimForeground` command for the slot the panel and the artifacts
panel share, and `handOffComposerText` for a continuation's unsent words. Two
hosts mount the same adapter — the conversation view and the shared-link view.

Opening the panel was three copies of "clear the artifacts slot, then select";
that is now `useOpenSubagentPanel`, and the three call sites say what they mean.

Nothing under `Chat/Subagents/**` imports `~/store` or `recoil` any more, and
its whole suite runs with no `RecoilRoot` at all.

* 🧹 refactor: Move Subagent State Into the Feature and Free Its Drained Atoms

Two review findings, both fair.

The selection atom still lived in `~/store`, which handed the supposedly
liftable surface an implicit dependency on the application store alongside its
declared host interface — and contradicted the ownership rule this branch
added to CLAUDE.md two commits ago. The module moves to
`components/Chat/Subagents/state.ts`; the hosts that write it (the SSE step
handler, the message renderers) now import from the feature, which is the
direction that survives an extraction.

The conversation-switch drain blanked its two atom-family members but left
them cached. Every invocation key is unique, so a long-lived tab accumulated
two atom configurations per subagent call it ever saw — exactly what the
comment at that boundary says it exists to prevent. `removeSubagentProgressAtoms`
releases them after the values are cleared, mirroring `removeUsageAtoms`. The
new test fails without the release.

* 🔎 fix: Let a Subagent Card Render Where There Is No Panel to Open

Search results render the same card through `Part`, and that route is not
beneath either chat surface — so a search hit containing a subagent tool call
threw on `useChatSurface()` before it could render at all.

Rendering the card and opening a panel are different requirements. The host
stays required for the panel, the activity view and the event group, all of
which only ever mount inside one. `useOpenSubagentPanel` reads the context
optionally instead and answers `null` where there is no host, which is also
the honest answer for search: there is no panel on that route to open, so the
card renders with its details control disabled.

The new test renders the card with no surface at all, and throws with the
reported error when the hook is put back the way it was.

* 🧮 fix: Enrol Read-Created Progress Atoms in the Drain Registry

The read is what creates a family member, and only the streaming paths were
registering their keys. A card rendering a conversation that finished
streaming long ago — or a search result that never streams at all — created
one the navigation drain could not see, so browsing history accumulated an
atom configuration per card for the life of the tab.

`useSubagentProgress` does both halves: it reads the invocation's progress and
enrols its key, so every route into the family shares the one cleanup
boundary. The panel and the parent card both read through it now.

* 🧽 fix: Free a Read-Created Progress Atom at Its Own Card's Unmount

Enrolling read-created keys in the drain registry only helps where something
runs the drain, and `/search` is a standalone route that never mounts the chat
event handlers. Scrolling a virtualised result list therefore kept minting
members nothing would ever free.

`useSubagentProgress` cleans up after itself instead: on unmount it frees the
family members when the read is the only reason they exist. Two guards keep
that from taking live history with it — a key the stream registered belongs to
the drain, and a member holding folded activity is the record of what the child
did, so reopening its card still shows it.

* 🔗 fix: Free a Progress Atom Only After Its Last Reader Lets Go

Opening a foreground panel before the first event gives one still-empty member
two readers: the card that spawned the child and the panel showing it. Closing
the panel freed it while the card was still subscribed — and a subscriber does
not re-render just because its atom left the family, so the first SSE update
landed on the replacement member and the card's ticker stayed dead.

`useSubagentProgress` counts its mounted readers and frees only when the last
one lets go, alongside the two claims that already blocked it: a key the stream
registered, and a member holding folded activity.
2026-09-01 11:15:42 -04:00
Danny Avila
a34d5dbfc7
🪂 fix: Preserve Subscribers Through Terminal Handoffs (#15462)
* fix: preserve fenced terminal subscriber handoff

* test: parse replay conversation pathname

* fix: capture late terminal subscribers
2026-09-01 11:15:24 -04:00
Danny Avila
c8939c9730
🪄 fix: Reuse Downloaded Artifacts for Image Reads (#15454) 2026-09-01 10:55:53 -04:00
Danny Avila
8f6228095f
🖼️ fix: Stop Reporting a Failed Sandbox Image Read as an Unreadable Format (#15453)
* 🖼️ fix: Stop Reporting a Failed Sandbox Image Read as an Unreadable Format

`read_file` on a `/mnt/data` image collapsed every failure — rate limit,
missing path, truncated transfer, transport error — into the binary-file
hint, whose image branch reads as a permanent capability limit ("is an
image file and cannot be read as text"). The model stopped retrying, and
the operator had no way to tell which failure had actually occurred.

The failure that surfaced it: the reader windows the bytes through
`/exec` stdout, one call per window, against the Code API's per-user
execution limiter (20 requests / 30s in the reference deployment). At a
fixed 32KB window a 122KB chart cost 4 calls and a 1MB image cost 33 —
more than a whole limiter window, so anything over ~640KB could never
complete. A turn that generates several charts and reads them back
exhausts the budget partway through and reports the 429s as unreadable
images.

- `handlers.ts`: failed reads name the real cause and the affordance that
  matches it — rate-limited waits and retries, a missing path lists the
  directory, a truncated transfer retries. The "cannot be read as text"
  phrasing is now reserved for bytes that are not a supported image. An
  image past the inline budget says to downscale and read the smaller
  copy rather than pointing at `bash_tool`, which cannot show an image.
- `process.js`: the window is derived from the runner's stdout budget
  (48,960B vs 32,768B, so a 122KB chart is 3 calls and 1MB is 22), halves
  and re-reads the same offset when a runner truncates anyway (remembered
  per Code API, with the env remedy logged), waits out a rate limit it
  can afford instead of discarding the bytes already pulled, and stops at
  a 24-call ceiling so no read stalls a turn across limiter windows.
- `utils/code.ts`: shared `getCodeApiRetryAfterMs` (header, then the
  `retry_after_seconds` body); `skillFiles.ts` drops its private copy.

* ♻️ refactor: Move the Sandbox Image Reader Into TypeScript

Addresses the review round on #15453.

- P1: windowing, window sizing, the in-sandbox reader script, response
  parsing and byte assembly move to `packages/api/src/files/code/image.ts`,
  and the Code API rate-limit policy to `packages/api/src/utils/code.ts`.
  `process.js` keeps only the `/exec` transport it owns. Both new modules
  are tested against their real implementations (19 + 13 cases), including
  the round-trip ceiling, the changed-file guard and the shell-injection
  guard on the reader script.
- P2: a configured stdout budget below the old 8KB window floor produced a
  window the runner could never emit, and narrowing was pinned to the same
  floor, so no retry could succeed. The window now aligns to the base64
  group size instead, and the round-trip ceiling bounds what a small budget
  costs.
- P2: `.env.example` documented a 32,768-byte default that no longer holds
  and never mentioned the stdout budget. It now describes
  `LIBRECHAT_CODE_SANDBOX_OUTPUT_MAX_SIZE` as the setting to tune, with the
  chunk override as the escape hatch.
- P3: `process.spec.js` no longer reimplements the retry parser in its
  module mock. Behaviour is tested where it lives; the mock is a declared
  passthrough and the spec covers what the transport still owns — session
  forwarding, the profile header, parser hand-off and error logging.
- P3: `getCodeApiRetryAfterMs` reads the response body through `in`
  narrowing rather than an `as` assertion.
- P3: the window-sizing JSDoc no longer claims a multiple of 3 in the one
  case that is not — an explicit `LIBRECHAT_CODE_IMAGE_CHUNK_BYTES`.

* 🎯 fix: Size Sandbox Image Reads to What the Limiter Can Deliver

Addresses review round 2 on #15453.

- Narrowing now halves the caller's own failing window instead of the
  shared learned value, and the learned value only moves down to the
  narrowest size any read needed. Reads run concurrently (one per tool
  call), so two reads failing at the same size were compounding into a
  window two steps smaller than the runner would have accepted, and
  persisting it for every later read.
- Narrowing continues until a runner accepts a window or a floor below
  which no runner could serve an image at all. Two halvings stopped at
  12,240 bytes — still ~16KB of base64 — so an 8-10KB stdout cap that was
  not configured up front stayed unreachable.
- The round-trip ceiling is now one limiter window (20), and a read that
  cannot finish within it says so from the FIRST response rather than
  spending the whole budget rediscovering it: the first window reveals
  both the file size and what a call really delivers. A near-limit image
  no longer drains the turn's executions before failing, which is what
  made the wait budget look too small.
- The round-trip message names the size this deployment can actually
  inline, so the model has a downscale target instead of a guess.
- `python3: not found` from the sandbox reader was matching the generic
  "not found" in `isSandboxMissingFileError` and being reported as a
  missing image path, sending the model to `ls /mnt/data` while hiding a
  runner dependency. Image reads now classify only path-specific ENOENT
  forms as a missing file.

* 📐 fix: Size a Truncated Read's Retry From the Cap It Revealed

Addresses review round 3 on #15453.

A ~1KB sandbox stdout cap is a real runner default, and the 1,536-byte
narrowing floor sat above it: discovery stopped one step short and
declared every image unreadable, even though small ones fit comfortably
in the 576-byte window the configured path already derives.

Narrowing now runs until no smaller window exists. It also no longer has
to grope its way down: a truncated response emitted exactly as much as
the runner allows, so its length measures the cap this deployment never
declared, and the retry is derived from it directly — one round-trip
instead of the eight blind halvings it takes to fall from ~49KB to ~1KB.
Blind halving remains the fallback and bounds the result either way, so
every retry is strictly smaller than the window that just failed.
2026-09-01 09:08:01 -04:00
Danny Avila
cdfe54c349
🚦 fix: Downgrade Stale Generation Fence Telemetry (#15452)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (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
Docker Dev Images Build / publish (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: downgrade stale generation fence telemetry

* fix: preserve fenced persistence failures

* fix: avoid duplicate fence warnings

* fix: propagate terminal publication fences

* style: format fencing audit changes

* fix: keep successor transitions generation scoped

* fix: bound predecessor handoff retirement

* fix: reconcile discovered successors completely

* fix: preserve successor handoff acknowledgement
2026-09-01 03:00:28 -04:00
Danny Avila
97e1e950ad
🧲 fix: Draw OpenID Refresh Lookups Into System Context (#15451)
* fix: scope refresh user lookups to system context

* fix: preserve refresh system context

* fix: bind refresh recovery to user tenant
2026-09-01 02:02:21 -04:00