Skip to content

Prepare LibreChat v0.8.8 compatibility candidate - #6

Merged
dschenk merged 1175 commits into
mainfrom
agent/prepare-librechat-v0.8.8-rc1-1217541308456676
Oct 1, 2026
Merged

dschenk merged 1175 commits into
mainfrom
agent/prepare-librechat-v0.8.8-rc1-1217541308456676

Conversation

@dschenk

@dschenk dschenk commented Aug 18, 2026 •

Copy link
Copy Markdown
Member

Prepares Gridonic's compatibility candidate on official v0.8.8, published 1 October and still marked prerelease at review. This replaces RC4 in the existing preparation PR; it does not authorize production rollout.

Rollout task · Configuration PR #17 · Acceptance and maintenance checklist

Why and exact delta

Gridonic currently runs patched v0.8.7. The release includes almost all those fixes. Only LC-005 still needs an implementation patch: a sessionless Streamable HTTP MCP can have working POST requests while its optional SSE GET fails. Upstream incorrectly emits a connection error for that optional-channel failure.

  • Keep the usable POST channel; preserve auth, POST, active-session and legacy-SSE failure behavior. Generic transport handling, not a DatoCMS exception. Upstream PR #14351.
  • One implementation file plus three regression suites. Nested delegation and completed-response recovery implementations are upstream; retain their regression coverage.
  • The RC4 dependency-security backport 🔩 chore: Raise Vulnerable Dependency Floors LibreChat-AI/LibreChat#16474 is now upstream and was removed. Dependency manifests, lockfile and Dockerfile.multi are unchanged from the release.
  • Negative control: all three optional-GET cases fail on unpatched v0.8.8, including with logger assertions removed. Restoring the patch makes them pass.

Upstream base: e8f3be08623663d4ad7f7241e693c94469b63bb0.
Tested head: cfc473cba4c778ae80c7cc280822a2792f651f90.
Rebased the existing branch; no merge commit or rewrite of main.

Patch retirement

LC-001/003/004/007/008 implementations are upstream. LC-002 becomes a configured-UID runtime-readability check. LC-006 is replaced by upstream Dockerfile.multi. Retire production patch-registry entries only after accepted rollout. LC-005 remains until its reproduced defect is fixed upstream.

Validation of this head

  • Clean install, package/frontend builds, API/client TypeScript, focused ESLint/import-order/diff checks pass.
  • 432 focused tests pass: MCP/OAuth/nested/credentials/document encoding (141), fetch/registry/skills boundaries (180), response recovery (111). OAuth tests emit an open-handle warning but exit successfully.
  • Production lockfile audit: zero known vulnerabilities; not a full security assessment.
  • Isolated upstream Lighthouse lane: median LCP 4.23s / CLS 0.017 / TBT 151ms, all within upstream budgets.
  • Full Gridonic YAML and 21 generated agent skill/subagent policies pass native validation.
  • Committed-source image built with unchanged upstream Dockerfile.multi: gridonic/librechat:v0.8.8-gridonic.cfc473cba, 653,697,771 bytes. Local ID: sha256:41f1f6adeb268f6b5b9b47381d6e5b0b70a723535c42151fafcb3f2392cebf4f.
  • UID 1001:1001 runtime readability/startup and isolated Gridonic integration pass: reconciliation/avatar idempotence, ACLs, skill visibility, non-CMS requests without DatoCMS, positive nested CMS, exact report transport, uploads/access isolation and same-process Ask User.
  • Real tenant-index CLI and dump/restore pass on 13 synthetic legacy-indexed collections, including failure safety and idempotence.

Remaining rollout gates

Fresh GitHub CI is running on this head: 12 passing checks, 32 queued/running at the latest snapshot. The inherited axe-linter failure is confirmed: GitHub cannot resolve dequelabs/axe-linter-action@v2, before repository checks execute. Review the remaining checks before approval; old RC4 results do not transfer. Both PRs remain draft. No automatic reviewer requested.

Production backup/identity refresh and migration need a separately approved maintenance window. Real provider/OAuth, browser, RAG and publication acceptance remain. The approved lean local-backup/fix-forward plan remains; no production backup or recovery-time claim. Paused conversations still cannot survive API restart without durable job storage; this accepted limitation is documented.

No merge, production deployment/restart, live credentials/data access or production migration. Disposable rehearsal resources were removed; only the local candidate image is retained for the planned rollout.

@dschenk
dschenk force-pushed the agent/prepare-librechat-v0.8.8-rc1-1217541308456676 branch from 1f89549 to 7434ceb Compare September 3, 2026 05:30
@dschenk dschenk changed the title Prepare LibreChat v0.8.8-rc1 compatibility baseline Prepare LibreChat v0.8.8-rc2 compatibility candidate Sep 3, 2026
danny-avila and others added 7 commits September 14, 2026 20:58
* fix: preview extracted upload text

* fix: preserve extracted-text preview metadata

* fix: complete extracted preview recovery paths

* fix: retain preview metadata across attachment recovery

* fix: preserve preview metadata across legacy steering

* fix: hydrate legacy steer preview metadata

* fix: hydrate reconnect steer metadata

* fix: preserve text preview metadata in resumed shares

* fix: hydrate legacy preview metadata safely

* fix: close preview lifecycle and recovery gaps

* fix: preserve preview ownership and wire metadata
…#15940)

* 🪂 feat: Opt-In Text Fallback for Files No Tool Can Read

An upload routed to tools (`llmDeliveryPath: none`) reaches nothing when the
agent handling the turn runs neither Run Code nor File Search, or runs one that
cannot read the type. With `fileConfig.endpoints.<endpoint>.textFallbackWithoutTools`
(also accepted at the top level and inherited from `default`), such a turn
delivers the text extracted at upload instead. The setting is off by default,
which keeps today's behavior.

- Upload: when the setting is on and an inferred `none` route finds no reading
  tool, the built-in document parser or native text reader stores the text on
  the file, best effort, under the extracted-text size cap and content policy.
- Turn: `resolveTurnLLMDeliveryPath` re-resolves each inferred route against
  the tools the agent runs, and `applyTurnTextFallback` marks copies of the
  records a turn loads, so endpoint filtering, limits, inspection, history
  replay and steer media all see the text the turn delivers. The stored route
  is never rewritten.
- Explicit destinations, records predating routing, and turns whose tools are
  unknown are left as they are.

* 🧪 test: Resolve Custom Endpoint Fallback Under the Endpoint Name

Uploads route a custom endpoint agent by its saved provider, the endpoint name. initializeAgent marks turn copies before getProviderConfig swaps the provider for the backing client, and the endpoint keeps the name afterward, so the client and child encoders resolve the opt-in under the same endpoint. Cover both the initialization path and the post-initialization agent shape.

* ♻️ refactor: Share One Extracted-Text Storage Cap Across Uploads

The fallback defined its own copy of the 15 MiB cap context uploads already apply to extracted text. Export MAX_STORED_EXTRACTED_TEXT_BYTES from files/extract and use it in both places, documenting that it bounds what a MongoDB file document can hold rather than operator policy.

* 🧭 fix: Keep Fallback Text Wherever a Later Turn Can Deliver It

Extraction skipped uploads filed under a reading tool, so a handoff agent
without that tool, or the same agent after its tools or grants changed, had
no text to fall back to. Store fallback text for every inferred route on a
message attachment; files kept on an agent's tool resources never reach a
prompt and still skip it.

Turn marking only ran while the fallback was enabled, so a stored tool-routed
record whose type the endpoint now routes to text stayed marked `none` and
`extractFileContext` skipped it. applyTurnTextDelivery (renamed from
applyTurnTextFallback) now marks any stored `none` record with text that the
turn resolves to text, and returns early when no such record is loaded.

* 🚦 fix: Admit Every Attachment by the Route Its Turn Delivers

A record upload routed to tools that the current endpoint sends to the
provider stayed marked `none` on the turn copy, so admission skipped it while
BaseClient encoded its bytes, bypassing attachment count and size limits.
applyTurnDelivery (renamed from applyTurnTextDelivery) now gives every record
whose route upload inferred the route the turn resolves, before endpoint
filtering, limits and inspection, matching the run-file encoder.
hasInferredLLMDeliveryPath shares the inferred-route rule with the resolver.

Fallback extraction selected the document parser inside the CJS upload path.
resolveUploadFallbackText now takes the upload and picks parseDocument or
parseTextNative itself, with the extractors injectable, so process.js passes
request data only. Its tests run the real parser against the xlsx fixture and
the real native reader against temporary files.

* 🎚️ fix: Read Custom Endpoint Dialect From Config on Both Sides of Init

initializeAgent materializes turn routes before getProviderConfig swaps the
saved custom endpoint name in agent.provider for its backing client, so
routing read that name as a non-OpenAI dialect. Media a custom endpoint opted
into could then be materialized off the provider path, skipping admission,
while the finalized client still sent the bytes. resolveAgentDeliveryRouting
now reads the dialect with getCustomEndpointProvider, as upload does, which
holds before and after the swap.

The upload fallback plan now keys on the chosen-destination marker the turn
resolver reads, so an upload recorded as chosen, including every legacy
chooser upload, never pays for text a turn cannot deliver.

* 🛡️ fix: Apply Turn Routes Only While the Record Stays Admitted

Resolution while initializing and at delivery can disagree: delivery reads a
Responses API choice only the finished client config holds. Applying a route
that takes a record out of admission, off the model path or to text it never
stored, would then skip the limits for a file the client still sends.
applyTurnDelivery now applies a route only while the record stays model-bound;
admitting a record the turn leaves out cannot slip past a limit.

* 🕹️ feat: Run Declared Project Actions on Attached Workers (LibreChat-AI#15943)

* feat: use declared attached project actions

* Complete named action definitions and approval previews

* Sort named environment imports

* Sort native environment fixture imports

* 🔓 fix: Keep Code Approval Mode Selectable Mid-Run (LibreChat-AI#15938)

* 🔓 fix: Keep Code Approval Mode Selectable Mid-Run

The composer's code approval mode selector was disabled while a run streamed, and a mode picked during a run was reverted when the final or abort event merged the server conversation back. The selector now stays usable in flight and both merges retain a mid-run selection, which the next send carries.

* 🔓 fix: Retain Mid-Run Approval Mode Across Cache and Recovery

Stamp the live approval mode onto the submission conversation at send so the retention baseline matches what was read, keep a retained mode in the conversation query cache on final and cancel, and rebuild failed or aborted conversations from a preset that carries the mid-run pick.

* 🔓 fix: Scope Retained Approval Mode to the Submitted Conversation

Retention now requires the live conversation to be the one the run submitted, or the id the server assigned to a new chat, so navigating elsewhere mid-run cannot write that conversation's mode back. A single recovery helper rebuilds failed or aborted conversations from the retained preset and patches the detail cache for every recovery site.

* 🔓 refactor: Keep the Local Approval Mode on Every Server Merge

Replace per-path retention with one rule: a locally picked code approval mode is newer than any server copy of the same conversation, so the final and abort merges and the error recovery preset keep it. The selector writes the pick into the conversation's detail cache, which navigation rebuilds from, so the pick is conversation-scoped without reading the index-global atom from the SSE handlers.

* 🔓 fix: Prefer the Live Conversation When Caching the Final Merge

The final handler's detail-cache merge now takes the local approval mode from the open conversation when it is the one that finished, falling back to the cached record only for a conversation that is no longer on screen.

* 🔓 fix: Keep the Local Approval Mode Through Settled-Start Reconciliation

The resumable transport's settled-start reconciliation and replacement handoff wrote a fetched conversation straight into conversation state and the detail cache. Both now apply the same local-mode rule as the event handlers, treating a new chat's pending id as the settled conversation's own.

* 🔓 fix: Seed the Detail Record When a Mode Is Picked

A conversation that has its id but no detail record yet now gets one from the live conversation when a mode is picked, the same key the resumable transport seeds optimistically, so recovery and navigation find the pick. A chat with no id yet still keeps the pick in conversation state alone.

* ⬆️ chore: bump agents to 3.8.7 (LibreChat-AI#15947)

* 🧭 refactor: Settle Turn Delivery Routing Once in Agent Initialization

`initializeAgent` resolved the provider and its client options only after the
turn's files were loaded and admitted, and the two delivery readers each rebuilt
the attachment routing from the agent object at their own moment: `BaseClient`
took the custom-endpoint dialect from the already-swapped `agent.provider`, the
run-file encoder from whatever child config it was handed.

Move `getProviderConfig`/`getOptions` ahead of file discovery, where nothing in
between fed them, and settle one `deliveryRouting` value with every input final:
the file policy under the endpoint's own name, the dialect its config declares,
the Responses API decision the model call uses, and the transcription setting.
`InitializedAgent`, the child encoder and `BaseClient` consume that value;
`resolveTurnLLMDeliveryPath` is the one place a stored route is resolved again.

* fix: Preserve attachment reachability across handoffs and late steers

* test: Type the untrusted attachment reference fixture

---------

Co-authored-by: Ravi Kumar L <upman@users.noreply.github.com>
* ⏱️ feat: Reveal Queued Follow-Up At Run Completion

In an agent conversation the server admits a queued follow-up on its own clock, and the client learned of that successor only through the receipt poll, the active-job poll and a resume attach, so the user's words appeared as the latest turn a second or more after the reply above them ended.

The queued follow-up is now drawn as the newest user turn the moment its predecessor completes cleanly, with its chip reduced to a remove action until the server admits it. The drawn turn is presentation intent only: it never enters the message cache, anchors no send, edit, fork or rerun, and shows only while the completed response it follows is the thread's tail. The intent ends once anything parents on that response, on a cancelled, dead or indeterminate receipt, or on the user's own removal. The composer queues behind it and the completed response offers no rerun while it stands.

The receipt poll also stays alive while the client holds a server-owned row the projection has not settled, so a successor that starts and finishes inside one active-job poll gap is no longer noticed only on the next focus.

* ⏱️ fix: Re-select the Reveal When Its Head Settles and Stop Polling Expired Rows

A revealed head that is cancelled or dies before admission leaves the server-owned queue non-empty and its terminal evidence outside the settled receipts, so nothing re-ran the drain for the row the backend moves on to; the drain now subscribes to the admissible head and the reveal itself. An uncertain row whose reconciliation window has expired is held for manual recovery only and no longer keeps the receipt poll alive.

* ⏱️ fix: Route Dictated Submissions Through the Composer's During-Run Path

A transcription completing while a queued follow-up was about to start called the send path directly and could race the turn the server already owed. The form's routing (answer mode, then steer or queue during a run or a pending reveal, then an ordinary send) now lives in one function that typed and dictated submissions share; a refused during-run submit keeps the text in the composer.

* fix: preserve queued turn ownership across admission handoff

* fix: preserve queued handoff lineage and ownership through recovery

* fix: delegate draft consumption and preserve refused prompts
* 🧷 feat: Carry Ask User Answers Past Compaction

The user's answers to `ask_user_question` lived only in the tool result that
received them. A checkpoint summary, the tool-output pruner or a full context
window could remove that message, and the agent then asked again or acted on
a summary's paraphrase.

Every answered ask on the conversation branch is now rendered verbatim into
the run's dynamic instructions, on the first request and on every resume,
where the answer just given is read from the seeded content. Older answers
drop first past a token budget; the newest set is always kept.

New lever: `endpoints.agents.askUserQuestion.retainedAnswers` (`enabled`,
`maxTokens`), on by default with a 4096-token budget.

* 🧷 feat: Carry Ask User Answers Past Compaction

The user's answers to `ask_user_question` lived only in the tool result that
received them. A checkpoint summary, the tool-output pruner or a full context
window could remove that message, and the agent then asked again or acted on
a summary's paraphrase.

Every answered ask on the conversation branch is now quoted verbatim at the
top of the current user turn's prompt copy, in the user's own role: rebuilt
from the stored rows every turn, never persisted, excluded from the memory
copy, and counted in the prompt total with the active encoding. A warm
event-actor turn reads the branch on demand. Older answers drop first past a
token budget that includes separators; the newest set is always kept.

New lever: `endpoints.agents.askUserQuestion.retainedAnswers` (`enabled`,
`maxTokens`), on by default with a 4096-token budget.

* 🧷 refactor: Read The Warm Branch Inside The Module

The warm event-actor read lived as a conditional and a `getMessages` call in
the CJS client. The typed module now takes `historyLoaded`, the identity and
the query as dependencies, decides when to read, and owns the failure policy
for both the read and the block itself; the client passes plumbing only.

* 🧷 fix: Complete The Branch Behind A Summary Cut

`loadHistory` already stops its walk at a checkpoint summary, so the rows
`buildMessages` receives never held the answers this feature carries. The
module now detects a branch that stops short of its root, whatever cut it,
and completes it through the injected stored-row query, stored rows winning.

The block goes on the latest user-authored row so a continued generation
keeps it, and the render measures the whole block before reserving the
omission note, so a set is only dropped when something truly has to go.

* 🧷 fix: Reuse Loaded History And Read Every Ask Stamp

A post-compaction turn read the conversation twice: once in `loadHistory`,
which cuts at the checkpoint summary, and again in the module to complete
the branch. The loader now keeps the rows it read and the module prefers
them, querying only for a turn that made no history read.

Two stored shapes were missed: a pause record whose `args` is the bare
question string, now read as the legacy question, and a call whose input
failed validation, whose persisted error output is no longer quoted as an
answer.

* 🧷 fix: Keep The Block Off The Stored Row And Out Of Memory Copies

Array-form content is replaced instead of edited through the array the
stored row shares, the block's cost is measured as two fresh counts of the
same copy rather than against a possibly calibrated stored count, the
memory copy is only built when memory extraction will read it, the loaded
rows reach the agents client through a hook and are released once the block
exists, and the tool-call-id merge is gone: rows are unique by message id
and providers may reuse ids across turns.

* 🧷 refactor: Apply The Answers Block Inside The Module

The placement lookup, the prepend, the count delta and the memory-copy
decision lived in the client after the context kickoff. They now live in
`applyRetainedAnswers`, which receives the rows, the prompt copies, the
count map, the counter and the memory dependency; the client keeps one
call and passes plumbing.

* fix: Apply Retained Answers After Summary Replay 🧷

* perf: Count Retained Context Exactly Within Its Budget 🧮

* fix: Preserve Retained Counts Across Checkpoint Recounts 🧾

* fix: Isolate Retained Context Counting and Provenance 🧵

* fix: Replace Warm Retained Context and Verify Answer Provenance 🪪

* fix: Use Workspace-Compatible Array Traversal 🧭

* fix: Refresh Retained Context at the Warm Turn Boundary 🧲
* fix: render agent descriptions consistently

* fix: harden marketplace descriptions

* fix: preserve rich description blocks

* fix: preserve angle-bracket description text
…reChat-AI#15952)

Registry names must be lowercase, so derive the GHCR image and Helm chart repository from a lowercased GITHUB_REPOSITORY_OWNER instead of github.repository_owner. Compare a11y PR heads against github.repository rather than a hardcoded owner, and remove the unused Azure self-hosted runner workflows.
@dschenk
dschenk force-pushed the agent/prepare-librechat-v0.8.8-rc1-1217541308456676 branch from 0e8f691 to 22dde90 Compare September 15, 2026 04:56
@dschenk dschenk changed the title Prepare LibreChat v0.8.8-rc2 compatibility candidate Prepare LibreChat v0.8.8-rc3 compatibility candidate Sep 15, 2026
danny-avila and others added 19 commits September 15, 2026 10:46
…5962)

* feat: carry schedule context into MCP token providers

* fix: restore scheduled token context after approval pauses

* fix: make scheduled identity boundaries explicit

* test: keep owner-drain leases live through CI delays
…at-AI#15963)

* fix: Preserve Code Environments Across Approval Pauses 🧷

* test: Match Agent Request Conversation Identity 🪪
* 🧱 fix: Keep Streamed Content When a Turn Fails

A failure the server reported outside the run — a rejected start on a resumed turn, an error SSE event, a lost connection — rebuilt the response row from the submission's empty placeholder, so the failure replaced the reasoning, text and tool calls that had already streamed and stood alone as if it were its own message. A failure inside the run never did this: the server appends it as an ERROR content part and the row keeps its parts.

resolveErrorTurn now builds every client-constructed failure the same way: the in-flight response keeps what it streamed, less stream holes and the empty text part a run opens before its first token, and takes the failure as one more ERROR part. A turn that fails before anything streamed is still the whole row.

* 🪪 fix: Keep Render Identity and Skip Empty Slots in the Failed Row

The streamed tail is compacted with isEmptyContentPart, so a comparison run's type: '' placeholders and a text or think part opened before its first delta no longer count as streamed content — a run that failed before either lane produced anything is the whole-row error again. Kept parts are stamped with preserveStreamedContentIdentity before the failure is appended, as the final path stamps them, so compacting a hole does not remount the settled row; the appended part is keyed past every existing key so a carried stamp cannot collide with it.

The spec fixtures are typed so the client typecheck passes.

* 🛤️ fix: Keep a Comparison Lane's Placeholder When the Other Lane Streamed

Empty slots now decide only whether anything streamed. Once something did, every non-hole part stays, so a comparison run whose one lane produced output before the failure keeps the other lane's type: '' placeholder — without it the parallel grouping saw a single agent and demoted the surviving output to the sequential flow under the primary agent's name.

* 🪪 fix: Keep the Streamed Row's Envelope When Appending a Failure

The failed row keeps the author, model, icon and creation time it streamed under and takes from the failure envelope only its metadata. A server payload names System as its sender and the schema dates a fresh envelope now; spreading either over the row changed its attribution and timestamp when only a part was being appended.
* 🪃 fix: Remember the Last Code Approval Mode

The code approval dropdown reopened on "Ask before changes" after every new
chat and reload. `useCodeApprovalMode` derived its selection from
`conversation.codeApprovalMode ?? 'ask'`, and a new conversation never carries
that field: `buildDefaultConvo` runs the last setup through `parseConvo`, whose
agents schema does not pick `codeApprovalMode`, so the pick was dropped.

The menu now records each pick in a localStorage-backed Jotai atom, and the
hook seeds its default from it when the conversation carries no mode of its
own. The remembered value is a preference and not a grant: it passes the same
policy gate as a stored mode, so anything current policy no longer allows
falls back to `ask`, and sign-in/sign-out clears it so a shared browser does
not hand the next account a permissive default.

* 🎨 style: Format the Code Approval Preference Files

---------

Co-authored-by: Lia <lia@librechat.ai>
…nts Module (LibreChat-AI#15910)

types/assistants.ts held ~60 exports of which only ~11 were Assistants
API types. The rest were core message-content types and the Agent entity
types, so schemas.ts and agents.ts both imported foundational types from
a file named after the Assistants API.

Split into types/tools.ts (tool primitives, action naming) and
types/content.ts (message content parts, annotations, tool calls), moved
the Agent entity types into the existing types/agents.ts, and left a slim
Assistants-only types/assistants.ts.

No behavior change: every symbol keeps its public export from the package
barrel, so no consumer outside data-provider changes.

Co-authored-by: Lia <lia@librechat.ai>
* 👆 fix: Thumb-Sized Send Button on Touch Devices

* fix: floor the submit target wherever touch is reachable, not only on a coarse primary pointer

---------

Co-authored-by: Lia <lia@librechat.ai>
…breChat-AI#15983)

* 🧦 fix: Restore OpenID Refresh After the Session Store TTL Expires

express-session raises `failed to load session` only when the store holds no
record for the session id. LibreChat-AI#15655 treated that as terminal, so a refresh whose
cookie refresh token was still valid ended in 401 OPENID_SESSION_MISSING once
the OpenID session store TTL (15 minutes by default) elapsed before the IdP
access token (~1 hour with Entra ID).

Reload the persisted record through one tolerant helper: an absent record is an
empty session, not a session that advanced past this result, so publication
seeds a new record. Store outages still propagate, and logout still fails a
retired refresh token through its durable revoked publication flight.

* 🧪 test: Pin the logout fence around the tolerated session reload

Tolerating an absent session record leaves the durable revoked publication
flight as the only thing between a logged-out refresh token and a resurrected
session, so cover both of its checks with an expired record: the tombstone read
that precedes the reload, and the completion that follows it.

Drop a needless optional chain in publishResolvedSessionTokens, whose `req`
parameter is not optional.

---------

Co-authored-by: Lia <lia@librechat.ai>
* 📓 fix: Keep Typed Text When Starting a New Chat

* 🧷 fix: Keep the Draft Claim Through the Empty-Files Write

---------

Co-authored-by: Lia <lia@librechat.ai>
)

* ⏮️ fix: Keep the Last Keystrokes When a Run Finishes

* 🧪 fix: Restore the Draft Mocks After the Real-Storage Block

---------

Co-authored-by: Lia <lia@librechat.ai>
…5992)

* fix: Enable programmatic Bash in selected attached workspaces

* Centralize project PTC construction and preserve recognized capabilities

* Prevent stale list refreshes from restoring removed pins
…Chat-AI#15982)

* 🎚️ fix: Allow Opting Out of the MCP OAuth `resource` Parameter

Adds `mcpServers.<name>.oauth.send_resource_parameter`, defaulting to true, so
operators whose authorization server rejects RFC 8707 `resource` can complete
the MCP OAuth flow. Suppression applies at send time only: Protected Resource
Metadata is still discovered, still validated against the server URL
(RFC 9728 §3.3) and still recorded on the stored client binding.

* 🧹 chore: Drop Accidentally Tracked node_modules Symlinks

Worktree-local symlinks were committed because .gitignore matches node_modules/ as a directory, not as a symlink of that name. They pointed outside the repository, so CI dependency installation failed and every downstream check with it.

* 🧪 test: Cover the MCP OAuth `resource` Opt-Out at Token Exchange

The authorization_code exchange reads the decision from flow metadata rather than live config, so a code obtained without `resource` is never exchanged with one. Covers both the opted-out flow and a flow initiated before the field existed, which must keep sending the parameter.

* 💅 style: Apply Prettier to the Token Exchange Test

* 🔒 fix: Make the MCP OAuth `resource` Opt-Out Actually Take Effect

Addresses three Codex findings on the opt-out:

- `startAuthorization` copies the configured `authorization_url` verbatim, so a
  `resource` an admin left in that URL survived into the request and Entra kept
  rejecting the flow. Opting out now deletes the parameter on both authorize
  paths instead of only declining to add one.
- `.omit()` let a user-managed submission carrying `send_resource_parameter`
  parse successfully while discarding the field, so the opt-out was silently
  ignored. It is now `z.never().optional()`, matching the documented contract
  and the `audience` precedent.
- A pending flow built with `resource` could be replayed for the full pending
  window after an operator set the option, reissuing the request they had just
  reconfigured away from. `isCurrentServerOAuthFlow` now compares the captured
  decision with live config. Stored tokens are unaffected, so changing the
  option still does not force re-authentication.

* 🧯 fix: Suppress Inherited MCP OAuth `resource` and Stale Replays

Addresses two further Codex findings on the opt-out:

- The MCP SDK uses `token_endpoint` verbatim and the refresh paths post to the
  resolved token URL as-is, so a `resource` an admin left in `token_url` (or one
  on a discovered token endpoint) still reached the provider. The outbound URL is
  now sanitized at the exchange and in all three refresh paths. Stored metadata
  keeps its configured form so the client binding still matches.
- Pending-flow replay was only guarded inside MCPConnectionFactory, so the
  browser initiate endpoint could still redirect to the stored URL. The shared
  pending helper now takes optional live config and refuses a replay whose
  captured decision no longer matches; the route passes it.

* 🪤 fix: Scope the MCP OAuth `resource` Opt-Out to Paths It Can Serve Correctly

The browser initiate route compared the pending flow against `resolveConfigServers`,
which deliberately omits unmodified YAML servers (see `mcp/context.ts`). For the very
config this option targets that read a captured `false` against `undefined`, reported
drift on every initiation, and then returned 400 because factory-written flow states
carry no `oauth` property to rebuild from. Reverted rather than patched: doing it right
needs effective-config resolution behind a `packages/api` helper, per the `/api`
wiring-only rule, and this sandbox cannot run that route suite.

Retained and corrected:

- The pending-flow guard no longer rides on `isCurrentServerOAuthFlow`, which also
  gates reuse of a recent COMPLETED flow. It was invalidating already-issued tokens,
  contradicting the promise that changing this option never forces re-authentication.
  Now a PENDING-only predicate at the two replay sites.
- `ServerConfigsDB.sanitizeUserManagedOAuthConfig` strips `send_resource_parameter`, so
  a DB-backed or imported user config cannot activate the admin-only opt-out.

---------

Co-authored-by: Lia <lia@librechat.ai>
…t-AI#15991)

* 🧼 fix: Strip Cache-Bust Query Before Local Vision Encode

Reused code-interpreter images persist filepath with a ?v= suffix.
prepareImagesLocal now strips it before disk encode, matching crud/share.

Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com>

* 🧪 test: Isolate prepareImagesLocal query-strip coverage

Mock sharp and resize so the local vision-encode test does not load
the rest of the file-strategy graph.

Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com>

* 🪒 fix: Strip Cache-Bust Suffixes From Every Local File Read

Reused code-interpreter outputs persist a `?v=<timestamp>` suffix on the file
document's filepath, and local storage resolves that field into a filesystem path.
prepareImagesLocal already handled it after the cherry-picked commits; getLocalFileStream
did not, so every download-stream consumer (the download route, provisioning, agent and
skill file reads) still hit ENOENT for a regenerated image.

Move the strip into one documented helper and call it from all three local read paths.

* 🧱 refactor: Move Cache-Bust Stripping Into the Storage Package

`/api` holds wiring, not behavior, so `stripCacheBust` moves out of a new CJS helper and
into `packages/api/src/storage/path.ts`, beside `resolveDownloadPath` — the function that
already decides what a stored filepath means to a download. Its doc comment now states why
the two differ: a remote strategy's query string can carry a presigned signature, so only
the local paths may strip.

The local storage modules keep just the call into it. Semantics are covered by the storage
package's own test; the `/api` specs pin the wiring.

Addresses the Codex P1 finding on 5f8dbf3.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com>
Co-authored-by: Lia <lia@librechat.ai>
…16008)

* Load authorized workspace repository instructions into agent context

* Unify repository instruction loading across agent ingresses

* Await cancellation regression assertion directly

* Annotate the shared instruction loader for declaration builds

* Expose repository read budget and inject the host-owned cache
* fix: Clarify Background Tool Handoffs and Validation Feedback

* Preserve background handle wire compatibility

* Align error logging regression assertions
…t-AI#15986)

* 🏛️ fix: Resolve Global Access Roles for Tenant Owner Grants

* fix: Execute the base-role fallback inside the system context

* fix: Resolve every tenant role lookup from one visible set

* fix: Merge the visible role set before any filtering

* perf: Resolve Visible Tenant Roles in One Query 🏘️

---------

Co-authored-by: lia-by-librechat[bot] <328778573+lia-by-librechat[bot]@users.noreply.github.com>
Co-authored-by: Lia <lia@librechat.ai>
Co-authored-by: Danny Avila <danny@librechat.ai>
…#15928)

* 🔌 feat: OpenAPI spec and docs for the public Agents API

* 🔧 fix: address review on Agents API OpenAPI spec and docs:

- adapter: mark JSON request bodies as required
- openapi.js: move gate, config read and spec serving into packages/api
  as an injected router factory; leave /api as wiring only
- docs: compute URLs in-browser and use a relative servers url so docs
  work under a base path (e.g. /chat)
- build: copy the generated spec into dist so the multi-stage image ships it
- agents: reuse the enforced list envelope for the docs so the AgentList
  schema cannot drift from the runtime cursor and strictness

* 🔧 fix: align Agents API OpenAPI contract with enforced runtime

Second review round on the public Agents API spec. Three cases where the
published contract described something the server does not do, so generated
clients would fail against the live API:

- Upload form: document the `purpose` field (enum: file_search, execute_code,
  context) that POST /agents/{id}/files actually requires, instead of the
  internal-only `tool_resource`; requests following the old form were rejected
  with 400 because `purpose` was absent.
- Auth: drop the `apiKeyBearer` scheme from the management and skill endpoints.
  They gate on OIDC/M2M via requireAgentManagementAuth and deliberately reject
  the API-key fallback; the API key belongs to the out-of-scope
  OpenAI-compatible endpoints and returns when those are documented.
- Skill update: document the 409 conflict PATCH /skills/{id} returns on a stale
  expectedVersion. The agent update path collapses its internal 409 to 400, so
  it correctly keeps no 409.

* 🔧 fix: serve docs on experimental server and document the 401

Third review round on the public Agents API spec.

- Experimental server: mount routes.openapi in api/server/experimental.js before its /api 404 handler. Without it, npm run backend:experimental returns 404 for /api/docs and /api/openapi.json even when openapi.enabled is true, because that server never mounted the router.
- Auth failure: document the 401 that requireAgentManagementAuth returns on a missing, expired, malformed, or unbound token. Its body is { "error": "Unauthorized" }, a flatter shape than the management error schema, so add a dedicated UnauthorizedError schema and a shared 401 response to the agent and skill contracts rather than reusing the standard error schema.

* 🔧 fix: document skill-update, auth-conflict, upload-limit, and watch-copy gaps

* 🔧 fix: added error shapes for permission denied and internal server errors in API responses docs

* fix: cover the auth-layer 500 shape and pin the account-deletion code

* fix: update OpenApiRouterDeps documentation and simplify request handling in isEnabled function

* 🔧 fix: gate the Swagger asset mount behind the OpenAPI toggle

* 🔧 fix: accept the malformed-JSON 400 envelope in the contract

* fix: accept the tenant-isolation 403 envelope in the contract

* fix: add description for content property in SkillFileUpdateRequest to clarify byte limit
…ns` (LibreChat-AI#16029)

Regenerates packages/api/openapi/agents.openapi.json from the code so
openapi:check passes again. LibreChat-AI#16008 added repositoryInstructions to the
agent schema and merged before LibreChat-AI#15928, whose committed spec was
generated from a branch that predated the field, leaving dev red.

Co-authored-by: Lia <lia@librechat.ai>
lia-by-librechat Bot and others added 26 commits September 29, 2026 20:54
…16531)

* 🧘 fix: Keep Prose Mounted While Activity Labels Update

* test: Advance Activity Ticker in Prose Identity Regression

* test: Cover Activity Prose Fade Stability in Browser

---------

Co-authored-by: Lia <lia@librechat.ai>
* feat: Scope YAML Custom Endpoints by Tenant

* fix: Hide Tenant Model Specs and Preserve Scoped Base Reads

* fix: Finalize Tenant Endpoint Isolation and Interface Defaults

* test: Complete Tenant Endpoint Schema Fixtures

* fix: Invalidate Merged Configurations When Tenant Scopes Change

* fix: Flatten Tenant Identity Resolution and Finalize Regression Coverage

* test: Match the YAML Endpoint Name Contract

* test: Use the Exact Custom Provider Name

* fix: Distinguish Missing Tenant Context in Override Cache Keys

---------

Co-authored-by: Lia <lia@librechat.ai>
…#16536)

* fix: bind per-user MCP API keys to request destinations

* fix: bind user keys after restoring OAuth destinations

* fix: include OAuth revocation endpoints in user key bindings

* fix: preserve MCP key readiness across equivalent requests

* fix: preserve explicit MCP API key-prefixed variables

* test: supply required MCP variable descriptions

---------

Co-authored-by: Lia <lia@librechat.ai>
* fix: Normalize Native File Edits Before Approval

* fix: Preserve Reviewed Edit Proposals on Resume

---------

Co-authored-by: Lia <lia@librechat.ai>
* fix: Rate Limit Authenticated 2FA Management

* fix: Explain 2FA Rate Limits With Localized Retry Guidance

* fix: Preserve Minimum 2FA Setup Budget
…at-AI#16572)

Every send moved the thread's tail (isSubmitting, latestMessageId,
latestMessageDepth, and a handleContinue rebound per turn), and the memoized
row context listed all of them as dependencies, so each move handed every row
a new context object and re-rendered the whole thread: three commits of
~12,000 component renders on a 246-message thread, one of them inside the
Enter keypress.

- Keep the row context object stable; the tail values are ref-backed getters
  for call-time reads, and rows receive them as props that the row comparator
  (isSameTailRelation) only reacts to when the row enters or leaves the tail.
- Fork reads the split target through a getter at click time.
- HoverButtons subscribes to a boolean isSubmitting context and a stable
  operations context instead of the combined messages-view context; Fork and
  Feedback are memoized so the per-send toolbar pass stops there.
- Make handleContinue referentially stable.
- Cache the Intl formatters behind getMessageTimestamp per locale and clock.
- Add a submit-lag benchmark (e2e/benchmarks-tree) that reports long tasks,
  Enter-to-row latency, a CPU profile and per-commit render reasons.
* chore: bump dompurify

* chore: bump agents sdk to v4.0.1
Co-authored-by: lia-by-librechat[bot] <328778573+lia-by-librechat[bot]@users.noreply.github.com>
@dschenk
dschenk force-pushed the agent/prepare-librechat-v0.8.8-rc1-1217541308456676 branch from f7638b8 to cfc473c Compare October 1, 2026 05:31
@dschenk dschenk changed the title Prepare LibreChat v0.8.8-rc4 compatibility candidate Prepare LibreChat v0.8.8 compatibility candidate Oct 1, 2026
@dschenk
dschenk marked this pull request as ready for review October 1, 2026 08:36
@dschenk
dschenk merged commit cfc473c into main Oct 1, 2026
44 of 46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.