merge#2
Open
im360john wants to merge 1991 commits into
Open
Conversation
Owner
Author
|
done |
* feat: Improve skill authoring guidance * test: Guard tool description lengths * fix: Align skill template guidance * fix: Satisfy advisory limit test lint * fix: Transform LangGraph ESM in Jest
…ted (#13525) * fix(projects): clear landing scope when the selected project is deleted When a project-scoped new-chat landing (/c/new?projectId=...) was open and the project got deleted, the chip kept showing the dead project and sends targeted it (saving unscoped with a visual glitch). - ChatRoute: only trust the scope when the project query succeeds (isSuccess), so React Query's retained-on-error data can't keep a deleted project's chip alive; strip ?projectId once the query settles to not-found so the landing reverts to a normal unscoped chat. - useDeleteProjectMutation: invalidate the project-detail query instead of removing it, so active observers refetch and settle into an error state (removing left them stuck loading under refetchOnMount: false). - e2e: regression test for delete-while-scoped. Fixes a follow-up issue to the projects feature (#13467). * fix(projects): only drop scope on definitive not-found; clear inactive deleted detail Address Codex review on #13525: - ChatRoute: gate scope removal on a 404 (isNotFoundError) or a success that resolves to a different/empty project, so a transient (non-404) failure under retry:false no longer unscopes a valid project; keep the chip through transient errors via retained data. - useDeleteProjectMutation: also removeQueries({ type: 'inactive' }) so a deleted project's inactive cached detail is dropped and a later visit refetches into a not-found state instead of rendering stale cache within cacheTime.
…3531) The Projects section defaulted to expanded, taking sidebar space for users with no projects. Now derive the default: collapsed when there are no projects and the user has never toggled the section; expanded once they have a project or explicitly expand it. Any explicit toggle (new projectsSectionToggled flag) — or a collapse set before this default existed — is respected.
* feat: scope model spec skills * style: format skill catalog limit * fix: serialize model spec skill resolution * test: satisfy model spec load config typing * fix: apply model spec skills to added conversations * fix: support alwaysApply frontmatter alias * fix: address model spec skills review
* feat: Add deployment skill directory * chore: Address deployment skill review feedback * fix: Include deployment skill file metadata * test: Add deployment skills e2e smoke test
* fix: add durable MCP config tombstones * fix: preserve scoped config tombstones * fix: clean up config tombstone lint * fix: handle empty model spec skill allowlist * fix: preserve inactive config tombstones
* fix: handle multiple MCP OAuth prompts * fix: address MCP OAuth review feedback * fix: address MCP OAuth prompt lifecycle review * fix: narrow OAuth prompt slot cleanup * fix: format OAuth prompt test --------- Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: reuse MCP OAuth authorization URL * fix: validate MCP OAuth initiate flow ID
* fix: filter ACL principal details * test: type ACL permission pipeline assertions * test: add ACL permissions e2e coverage
* fix: Support known endpoint group icons * fix: Resolve known endpoint group icon edge cases
* fix: Contain Markdown table overflow * fix: Improve Markdown table scrollbar
* fix: Preserve provider document uploads * test: Add provider upload e2e coverage
…Bump `@librechat/agents` (#13552) * 🔧 chore: Update ESLint config, add import sorting script, Test Sharding, Bump `@librechat/agents` * Change 'no-nested-ternary' rule from 'warn' to 'error' in ESLint config * Add new scripts for sorting imports in the project * Update lint-staged configuration to include import sorting * Modify GitHub Actions workflows to support sharding for unit tests * chore: remove nested ternary expressions * refactor: Extract scale multiplier logic into a separate function in CircleRender component * refactor: Simplify auto-refill rendering logic in Balance component for better readability * refactor: Improve width style handling in DataTable components for clarity and maintainability * chore: remove CircleRender component * delete: Remove CircleRender component as it is no longer needed in the project * chore: Bump @librechat/agents to version 3.2.31 and update Node.js engine requirement * Update @librechat/agents dependency from 3.2.2 to 3.2.31 in package-lock.json, api/package.json, and packages/api/package.json * Change Node.js engine requirement from >=20.0.0 to >=24.0.0 in @librechat/agents * chore: Add import sorting check to ESLint CI workflow * Implement a new job in the GitHub Actions workflow to verify import ordering on changed files. * The job checks for changes in specific file types and reports any import order drift, providing instructions for local fixes.
* fix: enable Gemini mixed tool config * fix: apply Gemini mixed tool flag after skills * style: match initialize formatting * style: wrap final tool check * fix: Respect Vertex auth mode * style: Sort Agent Initialize Imports * fix: Tighten Gemini Mixed Tool Gate
* feat: surface marketplace in model selector * chore: sort marketplace selector imports * fix: localize marketplace selector search
* feat: add soft default model spec * chore: sort ChatRoute imports
* fix: refine auth continuation handling * test: align auth route mock setup * fix: separate auth continuation throttling * test: format auth route mock * fix: preserve continuation limiter context * fix: hydrate continuation user before bans
* fix: Scope auth token lifecycle * fix: Preserve legacy auth token lookup * fix: Scope verification token cleanup
* fix: harden attachment usage handling * fix: sort file method imports * fix: clarify file usage scope
* fix: normalize verification flow responses * fix: keep verification responses consistent
* fix: Refine Agent Action Updates * fix: Format Action Update Helper * fix: Refine Agent Action Update Handling * fix: Move Agent Action Update Planning * fix: Sort Action Update Imports * chore: Reorder imports in actions.js for clarity
* feat: make OpenID token reuse window configurable via OPENID_REUSE_MAX_SESSION_AGE_MS
The OpenID session-token reuse window in AuthController was a hardcoded 15-minute
constant, forcing /api/auth/refresh to perform a real refreshTokenGrant against the
IdP every 15 minutes even when the current access token is still valid. IdPs that
rotate and revoke the previous access token on refresh then invalidate a token that
is still in use by downstream consumers of the reused OpenID token (e.g. MCP servers
that receive {{LIBRECHAT_OPENID_TOKEN}} and introspect the bearer), producing
~15-minute 401 cycles regardless of the access token's actual lifetime.
Read the window from process.env.OPENID_REUSE_MAX_SESSION_AGE_MS via the existing
math() helper, so it accepts an arithmetic expression like SESSION_EXPIRY (e.g.
60 * 60 * 24 * 1000), defaulting to the existing 15 minutes so behavior is unchanged
unless explicitly configured. The existing 30s-before-expiry guard still forces a
refresh before genuine expiry, so a larger window remains safe.
* fix: extend OpenID reuse session lifetime
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* feat: add opt-in Langfuse fanout collector * feat: fan out Langfuse feedback scores * docs: prepare Langfuse fanout for OSS setup * fix: clarify Langfuse fanout collector config * test: stabilize librechat suite * test: fix upload dialog import order * fix: omit empty Langfuse tenant fields * fix: gate tenant Langfuse fanout * test: cover central Langfuse env fallback * style: format Langfuse fanout config * feat: route langfuse fanout by destination * docs: clarify langfuse compose destination scope * test: remove unrelated suite stabilization * style: sort agent imports * fix: treat blank tenant fanout toggle as disabled * fix: rename tenant fanout emergency toggle * test: guard langfuse fanout collector config drift * feat: tune langfuse fanout batching * test: render fanout helm tests without dependencies * fix: narrow remote agent run config * refactor: share string normalization helper * fix: align langfuse fanout env parsing * fix(langfuse): align score fanout toggles with traces * fix(langfuse): keep central fanout config collector-only * fix(langfuse): type fanout collector config * fix(langfuse): harden tenant fanout config * feat(langfuse): support media fanout gateway * fix(langfuse): route tenant fanout through destination URL * fix(langfuse): harden fanout routing checks * ci(langfuse): test fanout gateway changes * ci(langfuse): check fanout go formatting * fix(langfuse): satisfy api typecheck
* 🐛 fix: route clipboard paste through upload-option guards Pasting a file skipped the composer's attachment guards, so unsupported types such as csv and xlsx reached the provider as document blocks and were rejected. Paste, drag, and the upload modal now share getViableUploadOptions to decide routing: zero viable destinations shows a toast, one auto-routes, several open the upload-type modal. * 🐛 fix: key ephemeral agent state by NEW_CONVO in upload-option flow useFileUploadRouter writes ephemeral capability state under `conversationId ?? Constants.NEW_CONVO`, but useUploadOptions and DragDropModal read it under `?? ''`, so on a new conversation the option resolver missed capabilities enabled by auto-routing. Align the reads on Constants.NEW_CONVO. * 🐛 fix: harden paste upload routing for assistants, custom endpoints, and toasts Bypass option resolution for Assistants endpoints on paste, matching drag-and-drop, so non-image assistant uploads use the assistants upload path instead of mis-routing to context or the unsupported toast. Honor a custom endpoint's configured supportedMimeTypes for direct provider attach instead of hardcoding image and PDF. Stop asserting upload success before validation runs; the single-route notice is now an informational "Attached as text" for the text-extraction case only. * 🐛 fix: refine paste upload routing for direct chats, custom endpoints, and disabled uploads Restore Code Interpreter and File Search options in direct and ephemeral chats by defaulting their permissions to allowed unless a saved agent omits the tool; selecting one still enables the ephemeral capability. Treat a custom endpoint as broad provider support only when its file config is permissive (matching the file picker), so an inherited default no longer offers zip/audio/video for direct attach. Short-circuit paste with the disabled-upload error before resolving options or opening the modal.
The getSharedLink query sorts by updatedAt, but the sharedlinks collection had no updatedAt index. Azure Cosmos DB for MongoDB (RU-based) rejects sorts on non-indexed fields, causing an immediate 500 on GET /api/share/link/:conversationId whenever a conversation is opened. Standard MongoDB is unaffected.
…3978) * harden token and remote file handling * sort s3 storage imports * split token submission rate limits
Opening the agent editor fetched the full `versions` array (each a complete config snapshot) alongside the agent, so agents with large histories were slow to open. Version history is now loaded only when the user opens it. - Add `getAgentWithVersionCount` (aggregation: version count, no versions array) and `getAgentVersions` data-schemas methods. - `getAgentHandler` returns the version count without the heavy array; add `GET /agents/:id/versions` (EDIT-gated) for lazy retrieval. - Add `useGetAgentVersionsQuery`; VersionPanel reads current config from the cached expanded query and fetches versions on open. Revert keeps the expanded cache and versions query in sync.
…#13975) Skill files were primed into the sandbox at `/mnt/data/{skillName}/...`, but the read_file/create_file/edit_file tool descriptions and the read_file bash-fallback hints all assume the `skills/{skillName}/...` namespace (sandbox cwd is `/mnt/data`). Agents therefore reached for `./skills/my-skill/...` in bash and missed ~100% of the time. - Add shared `SKILL_FILE_PREFIX` to agents/skills.ts (moved out of handlers.ts; single source of truth across the three layers). - Prefix the prime upload filenames and session names with `skills/` in skillFiles.ts so the physical mount matches the model-facing namespace; recover the bare relativePath by stripping `skills/{name}/`. - Canonicalize the read_file bash-fallback hints to `/mnt/data/skills/{skillName}/{relativePath}` so the implicit `{name}/...` addressing form is corrected too. Closes #13961
* 🚀 perf: Decouple Pinned Agents from Global Agents Map in Sidebar Pinned/favorite agents in the sidebar waited for the full global agents map (useListAgentsQuery, which walks every pagination cursor) before rendering. In environments with many agents this left pinned items in a loading state even though their IDs were already known. FavoritesList now fetches pinned agent IDs directly via getAgentById when the global map is still loading, and falls back to filtering only missing IDs once the map is available. The loading state tracks just the small set of pinned-agent queries instead of the entire catalog, so pinned agents appear as soon as their own data resolves. Closes #13967 * 🩹 fix: Address Codex review on pinned-agent decoupling - Stop caching the {found, agent} wrapper under the shared [QueryKeys.agent, id] key; direct fetches now return a plain Agent like useGetAgentByIdQuery, so opening/selecting a pinned agent within the stale window can no longer read a wrapper as an agent. Missing (404/403) agents are detected via the query error state. - Gate the direct fetches on the agents endpoint being enabled, so pinned agents the endpoint list intentionally hides are not fetched, rendered, or cleaned up when the endpoint is disabled. - Keep the loading skeleton while a direct fetch fails with a transient (non-404/403) error and the global agents map is still loading, so a pinned agent no longer disappears on a momentary 500/network error during startup. - Remove the now-unused AgentQueryResult type. * 🩹 fix: Address Codex round 2 on pinned-agent decoupling - Keep the loading skeleton (not an empty/collapsed row) while the endpoints query is still loading. The endpoint gate previously treated the default empty config as disabled, so pinned-agent favorites rendered an empty row that could be measured and cached by the CellMeasurer before the config arrived. isAgentsLoading now stays true while isEndpointsLoading is true. - Replace the blanket retry:false on direct pinned-agent fetches with a predicate that skips missing-agent (404/403) errors but still retries transient 500/network failures, restoring the prior default-retry resilience on the fast path. - Add data-testid to the favorite skeleton and a regression test for the endpoints-loading window. * 🛡️ fix: Don't delete pinned favorites on a global agents 403 GET /api/agents/:id runs the role-level AGENTS.USE check (checkAgentAccess) before the per-agent VIEW ACL, so a temporarily revoked role returns 403 for every agent. Because direct fetches now run while the agents map is undefined, treating those 403s as missing agents made the cleanup effect persist reorderFavorites and wipe all pinned agent favorites. staleAgentIdsKey now returns early while agentsMap is undefined, restoring the original invariant that favorite cleanup only runs once the global map has loaded successfully (which also proves AGENTS.USE is granted). Rendering of pinned agents while the map loads is unaffected; only deletion is deferred.
… Spacing (#13981) * 🪟 fix: Re-measure sidebar chat list on width change to fix date-group spacing When the sidebar is expanded from a collapsed reload, virtualized rows first measure mid-animation at a narrow width, so date-group headers wrap and cache an inflated height. CellMeasurerCache(fixedWidth) keys heights by row, not width, so the stale height persists once full width is reached — leaving gaps under headers. Invalidate the measurement cache and recompute row heights whenever the measured list width changes. Adds a Playwright mock e2e (seeds backdated convos across date groups via a new db helper) that fails without the fix and passes with it. * 🧪 test: Harden sidebar e2e (runtime-env path, midnight-safe seed, convo isolation) Addresses Codex review on PR #13981: - db.ts honors E2E_RUNTIME_ENV_PATH when locating the runtime Mongo URI. - Seed timestamps anchor on local noon so the Today group stays in-day near midnight. - Clear the shared user's conversations before seeding so later date-group headers are not pushed below the virtualized viewport by other specs' leftover chats.
… (Slice B) (#13942) * chore: add @langchain/langgraph-checkpoint-mongodb for HITL durable resume * feat: HITL tool approval runtime — backend (Slice B) - endpoints.agents.checkpointer config + durable Mongo checkpointer (seam over the app connection; SDK MemorySaver fallback) with a TTL index + deleteThread pruning - HITL run wiring (PreToolUse policy hook + humanInTheLoop) attached in createRun, fully inert when toolApproval.enabled is off - interrupt gate (pause job -> requires_action + emit on_pending_action) and a resume route that rebuilds the run from the durable checkpoint and run.resume()s it - atomic single-winner resolve; agent-consistency guard; expireStaleApprovals terminal event; checkpoint pruned on every non-paused completion (thread_id == conversationId) * feat: HITL tool approval UI — frontend (Slice B) approve/reject/edit/respond + ask-user controls in the tool card (OAuth-button precedent), batch-aware single submit, live + reconnect (resumeState.pendingAction) wiring, and resume mutations posting to /agents/chat/resume. * fix(hitl): decouple ApprovalProvider from chat context ApprovalProvider is now pure state (safe to mount in provider-less / shared / test renders); the context-dependent submit moved to a useResumeSubmit hook the cards call. Part imports getAskUserQuestionPart from ~/utils/approval directly so suites that partial-mock ~/utils render Part without throwing. * fix(hitl): address Codex review — backend - P1: enforce per-tool allowed_decisions on resume (reject a crafted decision the policy disallows) via findDisallowedDecisions - prune the durable checkpoint on user-abort of a paused run, and before a fresh HITL turn, so a new turn cannot rehydrate an expired/aborted interrupt (thread_id is the stable conversationId) - persist + use isTemporary and the original parentMessageId on resume (temporary chats stay temporary; initializeAgent scopes thread files off the right parent) - generate a deferred first-turn title BEFORE completeJob so its event reaches the client and the final event carries the real title - moderateText: skip when there is no text (tool-approval resume) and moderate the ask-user answer, instead of denying on an empty input * fix(hitl): address Codex review — frontend - render ToolApproval for ANY paused agent tool card (bash/code/file/etc.), not just the generic ToolCall, by wrapping the tool-card branch in Part (moved the rendering out of ToolCall) - findPendingActionMessageIndex only matches an assistant message, never the user message (the underscore-strip could target the user bubble before the assistant placeholder exists) * fix(hitl): address Codex re-review - title eligibility checks the user message’s parent (first turn), not the response’s parent — the previous check could never be true and skipped title generation - use client.buildResponseMetadata() for the resumed message so contextUsage / thoughtSignatures survive (the abort-only helper dropped them) - moderate decisions[].responseText (the respond action’s user text) - give /chat/abort req.config (configMiddleware) so the HITL checkpoint prune on abort actually runs - read resume state BEFORE setContentParts so the in-memory store does not lose the pre-pause seed content - count resumes against LIMIT_CONCURRENT_MESSAGES (increment/decrement) so paused-then- resumed turns cannot bypass the limit - require actionId on resume so a body without it cannot resolve the current action * fix(hitl): address Codex re-review (round 3) — resume fidelity Bring the lean resume path to parity with sendMessage for things it bypassed: - carry userMCPAuthMap into the rebuilt run so approved MCP tools keep the user's creds - seed initialSessions (buildInitialToolSessions) so approved code/file/skill tools have the pre-pause uploaded-file context (esp. cross-replica / after restart) - await client.artifactPromises and persist them as response attachments (else tool artifacts created after the pause vanish on reload / for late subscribers) - merge metadata: cumulative usage (+ summary marker) from the job, contextUsage / thoughtSignatures from the client — fixes the round-2 regression that underreported post-resume cost * fix(hitl): address Codex re-review (round 4) — resume hardening - resume: require an EXACT paused agent_id match (reject omitted/ephemeral agent_id, not just a different one) and reject an endpoint mismatch, so a request can't rebuild the claimed checkpoint on a different graph - moderateText: also moderate a tool-approval decision's reject `reason` and stringified `editedArguments`, not just `responseText` - request: re-mark the paused response `unfinished:true` after BaseClient saves it as completed, so an expired / never-resumed approval doesn't leave a "finished" response in history; the resume path overwrites it on success * test(hitl): route-level integration test for the resume controller Adds api/server/controllers/agents/__tests__/resume.spec.js, a supertest integration test that drives the real ResumeAgentController over the full pause -> approve -> resume -> finalize lifecycle with the SDK run, durable checkpointer, Mongo, and concurrency cache mocked. The pure decision/liveness helpers run for real via requireActual, so the guard ladder is exercised end to end rather than stubbed. 25 cases covering: - the authorization / staleness / agent-and-endpoint / actionId guard ladder - tool_approval validation (undecided tool call, policy-disallowed decision) - ask_user_question answer requirement - the concurrency gate (429) and the atomic single-winner claim (409) - the happy path: ACK, run reconstruction, decision->SDK mapping, finalize (save the now-finished response, emit done, complete job, prune checkpoint) - first-turn title generation before stream completion - re-pause (no double finalize), abort-during-resume (no double finalize), and the resume-failure terminal path (emitError + completeJob + prune) * test(hitl): strengthen resume coverage + add approval util tests Acts on a self-audit of the new resume integration test. resume.spec.js (25 -> 32 cases): - replace the tautological emitDone assertion (it only checked the hardcoded `final: true`) with a structural check of the finalEvent payload — responseMessage content/id/unfinished, requestMessage identity, title - cover the previously-unwalked finalize branches: tool-artifact attachments (null-filtered), the aggregatedContent fallback when live content is empty, and client response-metadata attachment - add guard cases: unsupported pending-action type (400) and the pre-multi-tenancy null-tenantId pass-through (must not 403) - add error-path cases: first-turn title generation throwing must still finalize, and a completeJob failure during a resume error must force a terminal job state via the last-resort updateJob client/src/utils/approval.spec.ts (new, 15 cases): - applyPendingAction tool_approval: join by tool_call_id not position, skip completed calls, default allowed_decisions to [], referential stability when nothing changes - applyPendingAction ask_user_question: append, idempotent replace on replay, non-array content coercion - getAskUserQuestionPart type guard; findPendingActionMessageIndex assistant-only resolution (never resolves to the user bubble) * fix(hitl): address Codex re-review (round 5) Five findings verified against the code before fixing: - resume: require an EXACT endpoint match (like agent_id) — a resume that OMITS endpoint must not fall through, since the shared chat middleware treats a missing/non-agents endpoint as the ephemeral agent and could rebuild the claimed checkpoint on a different graph - resume: filter malformed content parts before saving the finished response, matching the normal AgentClient path (a resumed turn could otherwise persist an empty/invalid tool_call part that breaks reload/rendering) - resume: accumulate tool artifacts across pause segments — persist them on re-pause and MERGE (not overwrite) at finalize, so artifacts produced before a second approval pause aren't dropped by the next rebuilt client - approval (client): findPendingActionMessageIndex returns -1 when a provided responseMessageId isn't found, so the caller retries instead of attaching the prompt/approval to a prior assistant reply; fall back to the last assistant only when no responseMessageId is given - RedisJobStore: make appendChunk extend-only (XADD + EXPIRE-if-shorter via a single eval) so the on_pending_action chunk emitted after a pause can't reset the chunk-stream TTL back to the running window and evict pre-pause content before the approval is resolved Tests: +endpoint-omitted/unsupported-type/malformed-filter/attachment-merge/ re-pause-persist cases in resume.spec.js (36); ask-retry -1 semantics in approval.spec.ts (16); extend-only TTL assertion in the RedisJobStore Redis integration spec. * test(hitl): mongodb-memory-server integration test for the checkpointer seam The checkpointer unit spec covers config/selection with no DB connection; this exercises the durable Mongo seam against a real (in-memory) MongoDB — the part correctness actually depends on: - getAgentCheckpointer builds a real MongoDBSaver when Mongo is connected and setup() creates the TTL index (expireAfterSeconds) on the checkpoint collection - memory type returns undefined (SDK MemorySaver fallback) even when connected - saver is memoized per resolved config - deleteAgentCheckpoint prunes a thread's persisted checkpoint (the cross-turn isolation guarantee: turn N+1 on the same conversationId can't rehydrate it) - pruning is thread-scoped — deleting one conversation leaves others intact - undefined threadId is a no-op * fix(hitl): address Codex re-review (round 6) Four findings verified against the code before fixing: - messageFilterPii: scan the resume payload's user-authored text (ask-user `answer`, and a tool-approval decision's `respond` text, `reject` reason, and edited tool arguments) — the shared /resume route ran through the PII filter but it only inspected req.body.text, so a blocked token rode the resume payload back into the model/tool (mirrors the earlier moderateText fix) - resume: re-prime skill files invoked in the pre-pause segment before rebuilding the run, so an approved code/file-backed tool keeps the injected skill-file session refs instead of running without them (mirrors the normal path's primeInvokedSkills; the pre-pause content stands in for the message payload) - hitl: pin the graph identity. Persist a fingerprint of the graph-determining request fields (endpoint, agent_id, model, spec, ephemeralAgent — normalized) on the pending action at pause, and reject a resume whose recomputed fingerprint differs. This closes the ephemeral-agent gap, where agent_id is undefined so the id guard can't tell two ephemeral configs apart - resume: reject incomplete edit/respond decisions (findIncompleteDecisions) — an `edit` without an object editedArguments or a `respond` without non-empty responseText is 400'd before mapping, rather than defaulting to {} / '' and resuming with behavior the user never approved Tests: incomplete-decision + fingerprint match/mismatch cases in resume.spec.js (41); findIncompleteDecisions + computeAgentRequestFingerprint unit tests; and resume-field PII cases in messageFilterPii.spec.ts. * fix(hitl): address Codex re-review (round 7) Four findings verified against the code before fixing: - RedisJobStore: clear `agent_id` on createJob (add it to staleHitlFields). The job hash is keyed by conversationId and reused across turns; updateMetadata only writes agent_id when truthy, so a conversation that switched from a saved agent to an ephemeral/no-agent turn kept the old id and the resume guard rejected the valid pause as a different agent. (real correctness bug) - fingerprint: include `promptPrefix` in computeAgentRequestFingerprint, and re-send it on resume (ResumeAgentFields + buildResumeFields). Ephemeral agents derive their system instructions from promptPrefix, so a resume changing it previously passed the pin and rebuilt different instructions. (completes the round-6 fingerprint) - resume: the re-pause branch now persists the segment's accumulated CONTENT (filtered), not just artifacts, so an approval that expires/reaps without a final resume no longer loses everything streamed during the resumed segment. - request: carry `manualSkills`/`alwaysAppliedSkills` on the persisted user message so a resumed turn's reconstructed requestMessage keeps its skill pills instead of dropping them until a full reload. Deferred (narrow, no safe contained fix yet — see PR thread replies): - resume rebuild without `addedConvo` for a multi-conversation/added-agent pane - cross-replica re-prime of manually-selected (not model-invoked) skill files Tests: stale-agent createJob clearing (Redis integration), promptPrefix fingerprint match/mismatch (resume.spec.js + policy.spec.ts), re-pause content persistence (resume.spec.js). * fix(hitl): address Codex re-review (round 8) Five findings verified against the code before fixing; the headline is a durable- resume correctness fix (the fingerprint had surfaced it as a 403): - resume durability (the important one): persist the graph-determining request fields (endpoint, agent_id, model, spec, promptPrefix, ephemeralAgent) on the pending action as `resumeContext`, and REPLAY them onto the resume request via a router-level middleware that runs before buildEndpointOption. The client can't reconstruct the ephemeral-agent config after a reload/cross-session, so the round-6/7 fingerprint would 403 a valid durable resume — and even without it the rebuilt agent would lose its tools. Replaying server-side rebuilds the SAME graph regardless of client state (and a crafted resume can't swap it; the fingerprint still matches because the body is restored first). - RedisJobStore: also clear `isTemporary` on createJob (same class as agent_id): a prior temporary turn's flag would otherwise survive a reused conversation hash and a later non-temporary resume would save its response as temporary. - resume: persist `contextMeta` (context-window calibration) onto the saved response like BaseClient does, so the next turn can seed its pruner. - request: carry manualSkills/alwaysAppliedSkills into the onStart metadata update (not just the preliminary one it overwrites), so a resumed turn's requestMessage keeps its skill pills. Deferred (narrow — see thread reply): - saved-agent edited WHILE a run is paused: agent_id matches but the definition changed; needs an agent version/config hash, which is a larger change for a narrow window. Tests: resumeContext pick/apply + round-trip (policy.spec.ts), contextMeta + manualSkills-on-requestMessage (resume.spec.js), isTemporary clearing (Redis integration). * style(hitl): prettier line-wrap in policy.spec.ts (R8 lint fix) * fix(hitl): address Codex re-review (round 9) Five findings, all fixed (addedConvo — deferred in rounds 7/8 — is now trivial thanks to the round-8 replay): - replay addedConvo: add it to RESUME_CONTEXT_KEYS so the resume middleware restores the parallel/secondary-agent config from the paused request; the client can't reconstruct it, and it determines the rebuilt graph. - skill pills (the real fix this time): the round-8 onStart metadata write was overwritten by trackUserMessage (the authoritative userMessage writer). Carry manualSkills/alwaysAppliedSkills in the emitted `created` message and persist them in trackUserMessage; widen UserMessageMeta + SerializableJobData.userMessage. - execute-code files on resume: seed the paused user message's own files onto req.body.files before initializeClient — they're excluded from the parent-walk code-session rebuild, so an approved code/read-file tool would otherwise resume without them. - in-memory pending-action UI: route ApprovalEvents.ON_PENDING_ACTION in the resume replay/pending-event loops to applyPendingActionToMessages (mirror the live handler), so a pause that lands in the snapshot window still renders its approval controls instead of sitting paused with no UI. - abort isTemporary: the /chat/abort partial-save now sources isTemporary from the job metadata, not req.body (the stop button posts only conversationId), so aborting a paused temporary chat no longer persists an orphaned partial. Tests: addedConvo in pickResumeContext (policy.spec.ts), file-restore on resume (resume.spec.js), abort-from-job-isTemporary (abort.spec.js). * fix(hitl): address Codex re-review (round 10) — resume/expiry races Three concurrency/coherence findings, verified against the code before fixing: - expiry-sweep CAS scope: both stale-approval sweeps (GenerationJobManager expireStaleApprovals and the RedisJobStore requires_action cleanup) called expire()/transitionStatus WITHOUT the observed pendingAction.actionId, so the CAS only checked status===requires_action. Between the read and the CAS a user could resolve the observed action and the run re-pause on a FRESH action; the stale sweep would then abort that valid new pause. Now both pass the observed actionId as expectActionId, so the CAS only fires for the action read as stale (a re-paused action has a different id → no-op). - resume graph cache: resumeCompletion cached the rebuilt graph (created with messages:[]) via setGraph; RedisJobStore.getContentParts prefers a cached graph over reconstructing from the chunk log, so a same-replica reload/status poll mid-resume returned aggregatedContent missing the pre-pause content. Skip setGraph on resume so introspection falls back to the complete chunk reconstruction (setContentParts still seeds the in-memory store). - pending-action UI: applyPendingActionToMessages scheduled a SINGLE animation-frame retry then dropped the pending action; Recoil/React updates can take several frames under load, leaving a valid requires_action run with no approval controls. Retry across frames (bounded at 120) until the target message commits. Test: expire() with a mismatched expectedActionId no-ops while the matching id expires (pendingAction.spec.ts). * chore(deps): update @librechat/agents to version 3.2.53 and @langchain/langgraph to version 1.4.7 in package-lock.json and related package.json files * refactor(hitl): add resolveToolApprovalPolicy seam for layered policy Extract the single point where tool-approval policy is resolved for a turn (`resolveToolApprovalPolicy`) and route the run call site through it instead of reading `endpoints.agents.toolApproval` inline. Behaviour-preserving: only the `endpoint` layer is wired today, so the result is identical to reading the app policy directly. The `agent` and `skills` layers are reserved seams with documented precedence (endpoint owns the `enabled` kill switch; agent overrides mode/allow/deny/ask/reason; skills may only tighten), so future per-agent and per-skill policy plumbing lands in one function rather than at the `createRun` site. Adds focused unit tests. * fix(hitl): address Codex re-review (round 11) — resume hardening F1 (P2, security) — applyResumeContext now DELETES any RESUME_CONTEXT_KEY absent from the persisted context, so the resume body carries exactly the graph-determining fields the pause had. Previously only defined keys were overwritten, leaving a client-supplied `addedConvo` (which the request fingerprint does not cover) in place — a crafted resume could rebuild a single-agent checkpoint as a different multi-agent graph/tool set. F3 (P2) — the resume route ACKs (res.json) before initializeClient, so a post-ACK getMCPRequestContext(req, res) saw the response as finished and returned undefined, leaving the resumed run without its run-scoped MCP connection store (approved MCP / OAuth-overlay tools then ran without their request-scoped connections). Pre-seed the store with a null res + cleanupOnResponse:false before the ACK and tear it down in the finally, mirroring the normal stream path (request.js). userMCPAuthMap was already preserved separately, so credentials were not lost — only the connection store. Declined: the ApprovalContext NEW_CONVO guard (P2) is a false positive — the `created` SSE event updates the conversation atom before any pause renders, so the id is concrete by click time (details in the PR thread). Tests: policy.spec (absent-key delete) + resume.spec (MCP context pre-seed/cleanup order). * fix(hitl): address Codex re-review (round 12) — resume fidelity + multi-tool UI F4 (P2) — temporal prompt vars: resume rebuilt the agent without restoring req.conversationCreatedAt or req.body.timezone, so {{current_datetime}}-style vars compiled a different system prompt than the paused graph (resume wall-clock, unzoned). Add 'timezone' to RESUME_CONTEXT_KEYS (persisted at pause, replayed by the resume middleware) and restore conversationCreatedAt from the convo before initializeClient — mirroring the normal path's resolveConversationCreatedAt. F5 (P2) — multi-tool approval: applyPendingActionToMessages stopped retrying once ANY tool-call part was tagged, so siblings that rendered on later frames never got approval controls and the resume route 400'd the partial batch. Add countTaggedApprovalParts and keep the bounded RAF retry going until every action_request is tagged (ask_user_question unchanged — one synthetic part). F6 (P3) — Edit accepted `null`/`[]` (valid JSON, non-object), enabling Submit for a value the resume route rejects via findIncompleteDecisions. Mirror the server's plain-object check in the client (store + editIsValid) so Submit only enables for an accepted value. Tests: policy.spec (timezone round-trip), resume.spec (conversationCreatedAt restore), approval.spec (countTaggedApprovalParts). * fix(hitl): address Codex re-review (round 13) — recurse into subagent approvals F9 (P2) — a tool paused INSIDE a subagent has its tool_call_id in the parent subagent tool_call's nested `subagent_content`, not as a top-level message part. applyToolApproval and countTaggedApprovalParts only scanned top-level content, so the approval never attached and the round-12 retry loop counted 0 tagged parts and spun to its frame cap with no controls. Both now recurse into `subagent_content` (immutably, so React refs update): the nested call gets tagged and is counted, so the retry terminates. Added approval.spec cases for the nested tag + count. Note: surfacing the interactive approve/reject controls inside the subagent view is a deliberate follow-up — ToolApproval -> useResumeSubmit -> useChatContext crashes when rendered in the portaled subagent dialog (outside the chat/approval providers), so that needs the controls scoped to the in-provider inline render (or the dialog wrapped with the providers). This commit fixes the data/traversal layer only. F7 (discovered-tool history on resume) and F8 (redis chunk TTL pause race) were verified false positives — see the PR threads. * fix(hitl): address Codex re-review (round 14) — resume fidelity + expiry relay F13 (P2) — manualSkills are graph-determining (skill allowed-tools union into the tool set before tools load) but weren't replayed, so a reload lost the skill tools and a crafted resume could inject a different skill past the fingerprint. Add 'manualSkills' to RESUME_CONTEXT_KEYS (same replay-only pattern as timezone/ addedConvo; the delete-absent half blocks injection). Not alwaysAppliedSkills — that's resolved server-side from the DB, not req.body. F12 (P2) — the resume final SSE built requestMessage from job.metadata.userMessage (persisted without files), so attachments vanished from the user bubble on resume. Spread the already-restored req.body.files onto it, matching the normal path. F11 (P2) — multi-replica approval expiry: RedisJobStore.cleanupRequiresActionIndex on another replica can win the requires_action->aborted CAS (it sets the hash error but has no event transport), and the local sweep then skips because the job is no longer requires_action, so a client subscribed here never gets the terminal error until the reap path. expireStaleApprovals now relays APPROVAL_EXPIRED_ERROR for a locally-subscribed job already aborted FOR approval expiry (error-string gated, idempotent via the errorEvent flag). emitError already publishes cross-replica. Tests: policy.spec (manualSkills round-trip + inject-drop), resume.spec (final requestMessage carries restored files). * fix(hitl): render approval controls for subagent-nested tool pauses (F10) Round-13 made applyToolApproval/countTaggedApprovalParts recurse into subagent_content (data), but SubagentDialogPart rendered nested TOOL_CALL parts with <ToolCall> only and never mounted <ToolApproval>, so a tool paused inside a subagent showed no controls and the run was unresolvable. Render <ToolApproval> in SubagentDialogPart's TOOL_CALL branch when the nested tool_call carries an approval and isn't yet resolved, mirroring the top-level Part.tsx render. The subagent dialog portals (OGDialog → ReactDOM.createPortal), but React context flows through the React tree, not the DOM tree, so ToolApproval resolves ApprovalProvider/ChatContext and the controls work + submit. Also harden useResumeSubmit: read ChatContext via useContext (non-throwing) instead of the throwing useChatContext wrapper, so the cards never crash when rendered outside a ChatContext.Provider (e.g. a search/citation render that passes chat context as a prop) — they degrade to inert (buildResumeFields returns null). * style(hitl): re-sort run.ts imports after dev rebase * fix(hitl): address Codex re-review (round 15) — resume content fidelity F14 (P2) — hide_sequential_outputs was applied in chatCompletion before saving/emitting content but not on resume, so a sequential-agent chain that pauses for HITL and resumes persisted/emitted intermediate outputs the setting is meant to hide. Extracted the filter into applyHideSequentialOutputsFilter() and call it from both chatCompletion and resumeCompletion (after handleRunInterrupt, covering the finalize + re-pause reads of client.contentParts). F16 (P2) — on a reloaded HITL pause, the DB already holds the paused user row + partial assistant row; useResumeOnLoad fed those as submission.messages, then finalHandler/createdHandler appended the same pair via requestMessage/responseMessage, duplicating the turn (buildTree doesn't dedupe children by messageId). buildSubmission- FromResumeState now strips the paused user/response rows (by messageId, incl. the padded/unpadded response id) from submission.messages — they're re-supplied by the placeholders + final event. Frontend-only; live (non-reload) pause path untouched. Deferred: F15 (collapsed-card subagent approval registration/visibility) — see thread. Tests: client.test (filter keeps last + tool_call parts / no-op when off), useResumeOnLoad.spec (paused pair stripped from submission.messages). * fix(hitl): address Codex re-review (round 16) — chunk TTL, slot, job replacement F17 (P2) — chunk-stream TTL on pause-before-chunk. CHUNK_APPEND_LUA derived its ceiling only from the chunk key's current TTL, so when the chunks key didn't exist at pause (fire-and-forget append in flight, or an ask-user pause before any chunk), the on_pending_action append created the stream with only the 20m running TTL while the approval window is 24h — content evicted before resume. The Lua now also reads the job key (KEYS[2]); when status == requires_action it takes max(running, TTL(jobKey)) (the approval window transitionStatus set), else the running TTL. Extend-only preserved; gated on paused status so normal runs never inflate. Both keys share {streamId} (cluster-safe). F19 (P2) — with LIMIT_CONCURRENT_MESSAGES, the approval prompt was emitted before the original request released its slot, so a fast Approve got /resume 429'd. handleRunInterrupt now releases the slot (idempotent via pendingRequestReleased) right after the pause, before the prompt; the request.js pause branch and resume.js finally only release if it didn't (no double-release). F20 (P2) — finalizeResumedTurn never checked the job wasn't replaced before emitDone/ completeJob/saveMessage, so a stale resume could clobber a newer turn that reused the conversationId. Added the createdAt guard the normal request path uses (skip finalization when the live job's createdAt != the paused job's). Deferred: F18 (subagent_content not reconstructed on Redis resume) — joins the subagent cluster (F15). See thread. Tests: RedisJobStore integration (pause-before-chunk gets approval TTL; running stays short), resume.spec (skip finalization on replacement; no double slot release on re-pause). * 🛡️ fix: Guard HITL terminal side-effects against job replacement Jobs are keyed by streamId == conversationId, so a new request REPLACES the running one on the same conversation. The replaced generation's tail must not clobber the live generation's state. Each path now re-reads the live job and compares createdAt against the generation's captured identity before acting. - Thread the generation's createdAt onto the client (request.js + resume.js) as client.jobCreatedAt — the identity every guard compares against. - handleRunInterrupt: skip approvals.pause when this run is no longer the live job, so a stale interrupt can't flip the NEWER job to requires_action. - chatCompletion finally: skip the checkpoint prune when replaced, so an older run's late finally can't delete the newer run's resume checkpoint. - resume catch-path: gate emitError/completeJob/prune behind a stillLive check (fail-open if the read throws), mirroring finalizeResumedTurn's success guard. - Persist the turn's uploaded files on job.metadata.userMessage (authoritative trackUserMessage writer) and prefer them on resume over the user DB row, whose save can still be racing a fast /resume. Tests: 13 guard-predicate cases in jobReplacement.spec.js. * 🔁 fix: Harden HITL resume — ownership re-check, file seeding, deferred-tool replay Three follow-ups to the round-17 job-replacement guards (Codex review 4594099963): - G1 (resume.js): the success-path ownership guard runs at the START of finalizeResumedTurn, but saveMessage + first-turn title generation await long enough for a new request to replace the job on the same conversationId. Re-read the live job immediately before emitDone/completeJob/prune so the terminal writes can't tear down the REPLACEMENT job — mirrors the catch-path guard. - G2 (request.js): onStart's metadata/chunk writes that persist the turn's files are fire-and-forget, so a fast approval could read job.metadata.userMessage before files landed. Seed files into getPreliminaryUserMessage instead — that write is AWAITED before the run starts, so files are durable before any interrupt can emit. - G3 (run.ts + client.js + resume.js + IJobStore.ts): the resumed graph is rebuilt with messages: [], so createRun's tool_search-discovery scan finds nothing. A deferred tool discovered earlier in the turn (and targeted by the paused call) was therefore absent from the rebuilt schema-only toolMap — resume would throw "unknown tool" (no loadRuntimeTools fallback is wired). Capture discovered tool names at pause via extractDiscoveredToolsFromHistory(run.getRunMessages()), persist them on job.metadata.discoveredTools, and replay them into createRun's new discoveredToolNames input (merged with message-extracted names, gated on hasAnyDeferredTools — inert otherwise). A new createRun test proves the deferred tool is promoted with the replay and absent without it (reproducing the bug). Tests: real createRun deferred-replay suite (run-summarization.test.ts) + G1/G2/G3 guard predicates (jobReplacement.spec.js). Full suite green. * 🔒 fix: Close HITL resume metadata + file-substitution + pause-race gaps Four findings on the round-18 commit (Codex review 4594430222): - H1 (P1, regression in round-18 G3): the discoveredTools captured at pause never reached resume — three metadata allowlists dropped it: GenerationJobManager .updateMetadata, RedisJobStore.deserializeJob, and buildJobFacade (plus the GenerationJobMetadata type). Added discoveredTools to all four, so the deferred-tool replay actually works end-to-end (in-memory store already kept it via Object.assign). - H2 (P2, security): /resume honored a client-supplied `files` array, letting a crafted client resume an approved code/read-file tool against a DIFFERENT file set than the one approved (files aren't in the resume fingerprint/context). Resume now ALWAYS sources files from the paused job (metadata → DB row), clearing any client-supplied set. - H3 (P2, ephemeral fidelity): non-default model parameters (temperature, max tokens, custom endpoint params) were lost on resume — ephemeral agents derive them from the request body, which the resume payload omits. Capture the resolved model_parameters in resumeContext at pause and replay them onto the body on resume (excluding `model`, which is replayed via the fingerprinted RESUME_CONTEXT_KEYS path). Saved agents already source these from the DB. - H4 (P2, Redis race): a pause landing between the resume snapshot and the Pub/Sub subscription reached neither resumeState.pendingAction nor (Redis) pendingEvents, and approval events aren't persisted to replayEvents — the client attached to a paused job with no approval UI. subscribeWithResume now re-reads the live job AFTER subscribing and surfaces the pending action if the snapshot missed it (live read, no staleness). Tests: discoveredTools metadata round-trip + subscribeWithResume re-read (pendingAction .spec.ts); client-file substitution rejection (resume.spec.js); model-parameter replay predicate (jobReplacement.spec.js). * 🧹 fix: Clear stale discovered tools, release slot on claim error, extend run-step TTL Three follow-ups on the round-19 commit (Codex review 4594783691): - I1 (P2): the round-19 discoveredTools field wasn't cleared on Redis streamId reuse. HSET only overwrites listed fields and handleRunInterrupt only writes discoveredTools when THIS turn discovers a deferred tool — so a replacement turn that pauses without its own discovery inherited the prior run's tool names and force-loaded undiscovered deferred tools on resume. Added discoveredTools to createJob's staleHitlFields HDEL list (the in-memory store already builds a fresh object, so it was Redis-only). - I2 (P2): with LIMIT_CONCURRENT_MESSAGES, approvals.resolve runs after the slot increment but before the run's try/finally, so a store/Redis error there leaked the slot until the counter TTL expired (spurious 429s on retry of the still-paused approval). Wrapped the claim in try/catch that decrements the slot and returns 500. - I3 (P3): saveRunSteps did SET ... EX running unconditionally, resetting the run-steps key to the 20-min running TTL even while the job is paused for the longer approval window — a reload after that window lost the tool timeline. Now uses a paused-window TTL script mirroring the chunk-stream no-shrink behavior (extends to the approval window when the job hash is requires_action). Also fixes a latent strict-tsc cast error in the round-19 pendingAction test. Tests: claim-throws-releases-slot (resume.spec.js); discoveredTools cleared on reuse + saveRunSteps preserves the paused TTL (RedisJobStore integration, USE_REDIS). * 🛡️ fix: Guard fast-resume save race, gate HITL to resumable routes, expire on stale submit Three findings on the round-20 commit (Codex review 4595045652): - J2 (P1): a fast /resume can claim + finalize the COMPLETED response while the original request's pause branch is still awaiting `response.databasePromise`; the later unfinished-save then overwrites the completed content. Re-check the job is still paused on THIS generation's action (a claim leaves requires_action; a replacement bumps createdAt) before marking the row unfinished; fail open on a read error. - J3 (P1): the tool-approval wiring (humanInTheLoop + PreToolUse hook + checkpointer) was applied to EVERY createRun caller when toolApproval.enabled, but the OpenAI-compatible and Responses controllers never inspect run.getInterrupt() or persist a pending action — an approval-gated tool would pause there with no approval surface or resume endpoint and the route would emit a normal final response / [DONE] with the tool call dangling. Gate the wiring on a new createRun `hitlCapable` flag, set only by AgentClient (chat + resume). - J4 (P2): a stale-action 409 on submit returned without driving expiry, leaving the job requires_action with a dead action until the periodic sweeper ran — any attached SSE client got no terminal event and the stream appeared to hang. Extracted GenerationJobManager .expireApproval(streamId, actionId) (expire CAS + terminal SSE, shared with the sweeper) and call it from the resume route when the observed action is stale. J1 (nested subagent approval controls not mounting while the details dialog is closed) is a valid frontend issue in the deferred subagent-HITL path — tracked separately (replied on the thread) since the fix touches the shared dialog primitive and needs UI verification. Tests: HITL-gate both directions (run-summarization.test.ts); expire-on-stale-submit (resume.spec.js); fast-resume unfinished-save guard predicate (jobReplacement.spec.js). * 💄 style: Wrap captureAgents signature to satisfy prettier (CI lint)
…uring streaming (#14011) * perf: memoize FavoritesList and BookmarkNav to prevent streaming re-renders ConversationsSection re-renders during message streaming as its conversation-list query and title generation update the cache. Its FavoritesList and BookmarkNav children were not memoized, so they re-rendered on every parent commit despite their props and subscriptions never changing during a stream. Wrap both in React.memo to insulate them from the parent cascade. Their props (toggleNav, isSmallScreen, tags, setTags) are referentially stable, so memo fully decouples them. Add a regression test asserting FavoritesList does not re-run when its parent re-renders with stable props. * test: verify ConversationsSection insulates Favorites/Bookmarks from streaming re-renders Renders the real ConversationsSection (mocking only data hooks) and forces repeated re-renders via a subscription it depends on, mirroring the conversation-list/title-generation cache churn during streaming. Asserts FavoritesList and BookmarkNav do not re-render, proving the parent passes referentially stable props so React.memo holds in the real render path (not just with hand-fed stable props).
* ✨ feat: Dock-Fisheye Message Nav Rail with Instant Hover Preview * 🎚️ refactor: Uniform resting ribs + clickable cursor for message nav * 🧹 fix: One rib per message in nav rail (dedupe nested message-render) * 🎯 fix: Accurate fisheye focus + click-anywhere-to-jump in message nav - Measure rib centers relative to the column (getBoundingClientRect) instead of offsetTop, which was relative to the positioned <nav> and shifted the pointer->rib mapping by the chevron height (hovered line wasn't the peak, preview showed an earlier message). - Column-level click jumps to the focused rib, so clicking anywhere the preview is showing works even when the pointer is off the thin line. - Restore @librechat/client jest stub to keep the unit isolated. * 💡 fix: Highlight only the hovered rib white in message nav * 🫥 style: Transparent message nav (drop pill background) * ♿ feat: Keyboard focus mirrors hover (magnify + highlight + preview) in message nav Tabbing to / Shift+Alt+M focusing a rib now drives the same fisheye pipeline as pointer hover via onFocus/onBlur on the column: the focused rib magnifies, highlights white, and shows the shared preview. Also addresses Codex finding on keyboard-focus previews. * 🩹 fix: Live tooltip preview + legacy media-query fallback in message nav - Derive the shared preview text from entryById at render time instead of snapshotting it into tip state, so a streaming/updating message refreshes the open tooltip without leaving and re-entering the rail. - Feature-detect MediaQueryList.addEventListener and fall back to addListener/removeListener so the reduced-motion watcher no longer throws (and breaks the nav) on Safari/iOS < 14. Addresses both Codex findings on review 4601236141.
At rest (no hover/focus) the rail now reads like a minimap: only the in-viewport message ribs are at full opacity while the rest fade to 40%, updating live as you scroll. Replaces the blanket nav opacity-30 with per-rib opacity driven by the existing viewport-visibility highlight, so hover/focus still brings every rib to full opacity for the fisheye.
* ✨ feat: Add Claude Sonnet 5 Support
Wire up the claude-sonnet-5 model across token, pricing, and model-list
config:
- Context window (1M) and max output (128K) in @librechat/api token maps
- Standard pricing ($3/$15 per MTok) and cache rates in data-schemas tx
- 128K output-token carve-out in anthropicSettings (the family-wide 64K
rule capped Sonnet 5 below its real limit); Bedrock/Vertex thinking and
1M-context detection already cover sonnet major >= 5 generically
- Add to shared Anthropic, Bedrock, and Vertex default model lists, plus
the .env.example examples
- Tests for context/output/pricing/matching across the affected packages
* ✅ test: Align Sonnet 5+ maxOutputTokens defaults with 128K spec
getLLMConfig defaults flow from anthropicSettings.maxOutputTokens.reset(),
which now returns 128K for Sonnet 5+. Update the future-proofing assertions
in llm.spec.ts (Sonnet 5.x and 6-9.x) that still expected the old family-wide
64K cap. Haiku stays 64K; Opus stays 128K.
* 🎚️ fix: Gate Sonnet 5 capability behaviors (sampling, thinking)
Adding claude-sonnet-5 to the default list exposed it without the Anthropic
capability gates, all confirmed against the live API:
- omitsSamplingParameters: Sonnet 5 returns 400 on non-default temperature/
top_p/top_k ('deprecated for this model'); now dropped so selecting the
model with saved sampling settings no longer fails.
- requiresExplicitThinkingDisabled: omitting 'thinking' runs adaptive ON by
default on Sonnet 5, so disabling thinking now sends { type: 'disabled' }
(verified: 200, no thinking block) instead of omitting the field.
- omitsThinkingByDefault: thinking.display defaults to omitted (empty thinking
blocks); the display resolver now returns 'summarized' for Sonnet 5+ so the
Thoughts UI keeps working (verified: 757-char summary returned).
Gates apply to both the direct Anthropic and Bedrock paths. Tests added in
bedrock.spec and llm.spec.
* 🩹 fix: Sonnet 5 Bedrock availability + thinking-off persistence
Round-2 Codex review (all verified against the live API / Anthropic docs):
- Sonnet 5 is NOT available on the legacy Bedrock InvokeModel/Converse surface
(Anthropic docs: 'use Claude in Amazon Bedrock or Claude Platform on AWS'),
which is what LibreChat's ChatBedrockConverse uses. Removed it from the
default Bedrock model lists (config + .env.example). Opus 4.8/4.7/Fable 5
stay — those ARE reachable via InvokeModel. Sonnet 5 remains on the direct
Anthropic API and Vertex, where it works.
- Reverted the Bedrock-side explicit-disabled thinking handling added last
round: with Sonnet 5 off Bedrock, no Bedrock model needs { type: 'disabled' },
so that path (and its round-trip concern) no longer applies.
- Direct Anthropic path: a persisted { type: 'disabled' } thinking object now
normalizes to a boolean flag in getLLMConfig, so a user's Sonnet 5
'thinking off' setting stays off across the model_parameters round trip
instead of flipping back to adaptive (a truthy object skipped the disabled
branch).
* ↩️ fix: Restore Sonnet 5 on Bedrock (Converse) — verified live
Reverses the round-2 removal: Sonnet 5 IS available on AWS Bedrock. Tested
live via the Converse API:
- global.anthropic.claude-sonnet-5 returns a normal response
- bare anthropic.claude-sonnet-5 needs an inference profile — but that's
identical to the already-shipping Opus 4.8 / Fable 5 / Sonnet 4.6 entries,
which all fail bare on-demand the same way
- temperature=0.5 -> 400 'deprecated for this model'; thinking {type:disabled}
suppresses reasoning — same as the direct API
The 'legacy' Bedrock docs page that claimed Sonnet 5 wasn't on the surface is
stale. Restored:
- anthropic.claude-sonnet-5 in bedrockModels + .env.example
- the Bedrock explicit-disabled thinking handling (requiresExplicitThinkingDisabled
-> { type: 'disabled' })
- the Finding 4 round-trip fix in bedrockInputSchema (coerce a persisted
disabled AMRF.thinking to thinking=false instead of !!thinking -> true), with
an end-to-end schema->parser test proving 'thinking off' stays sticky.
Direct-path round-trip fix (getLLMConfig thinkingFlag) is unchanged.
* 💵 fix: Sonnet 5 intro pricing + sticky disabled thinking on Bedrock reload
Round-4 Codex review (both verified):
- Pricing: Anthropic lists Sonnet 5 at introductory $2/$10 per MTok (cache
$2.50/$0.20) through 2026-08-31, reverting to $3/$15 ($3.75/$0.30) on
Sep 1 (confirmed on platform.claude.com/pricing). The static tx multiplier
table is used for real balance transactions, so the post-intro rates were
overcharging ~50% during the launch window. Switched to the intro rates with
a revert comment on both the token and cache entries.
- Bedrock disabled-thinking persistence: initializeBedrock feeds persisted
model_parameters straight through bedrockInputParser (NOT bedrockInputSchema),
where additionalModelRequestFields is a known key — so a prior
thinking:{type:'disabled'} was ignored and rebuilt as adaptive on reload.
bedrockInputParser now surfaces a persisted disabled AMRF.thinking as
thinking=false so it re-emits {type:'disabled'}. Verified end-to-end against
the real initializeBedrock call path.
) A large SKILL.md/file authored in one create_file call can exceed the model's max output token budget; the response is cut off before the content argument finishes, the arg is dropped, and the handler returns a bare 'content is required'. The model reads that as a forgotten field and retries the same oversized write, looping. Make the error actionable: tell the model the response may have been truncated and to keep the main file lean, moving bulky sections into separate files written in their own calls.
Add an optional `showInMenu` flag to model specs. When set to false, the spec is dropped from the model selector menu and from the client startup config (GET /api/config), but remains resolvable server-side by name — a request that sends `spec: "<name>"` still works, since server-side resolution uses the full, unfiltered list. Unlike `showIconInMenu` (which only hides the icon), this hides the whole entry. The flag is optional and defaults to listed, so existing specs are unaffected. Adds an `excludeHiddenModelSpecs()` helper (applied before `sanitizeModelSpecs`) plus unit tests.
…Documents (#14026) * fix: download original file from artifact preview panel for office documents The preview panel download button serialized the rendered HTML preview instead of the original binary for office artifacts (pptx/xlsx/docx) produced by the code interpreter, so users got an `index.html` text scrape rather than the file. The inline chat card was unaffected because it downloads the real file via `useAttachmentLink`. Thread the original-file download metadata (filepath/file_id/source/user) through `fileToArtifact` onto the Artifact, and update `DownloadArtifact` to fetch the original file through that same path for preview-only office artifacts. Text, source, and markdown artifacts keep the blob path so their in-panel content (and edits) still download as-is. Closes #14002 * fix: require a usable route before downloading the original artifact file A shared link to a non-snapshotted code-execution office artifact strips source/user and deletes filepath while keeping file_id (share sanitization + applyShareFileRoute). The preview-panel download gate treated that lone file_id as sufficient, so it routed to an empty useCodeOutputDownload fetch and downloaded nothing instead of falling back to the preview-content blob. Take the original-file branch only when useAttachmentLink can actually fetch: a non-empty filepath (http target, share route, or code-output URL) or full local-file metadata (isLocallyStoredSource + file_id + user). Export isLocallyStoredSource from LogLink so the panel reuses the same predicate. * fix: only show artifact download success after the file is delivered useAttachmentLink swallows fetch errors (an expired code-output URL or a 404 share download) and resolves without throwing, so the preview-panel download button flipped to the success checkmark even when no file was downloaded. Return a boolean from handleDownload (true once a download is initiated, false on error/empty response) and only mark the artifact download as succeeded when a file was actually delivered. The return value is ignored by the existing onClick callers.
The `@librechat/api` build migrated to tsdown (rolldown/oxc) in #13595. tsdown externalizes third-party deps and uses strict CJS interop, so a default import of the Firebase v9+ modular SDK — whose CJS entry is `__esModule`-marked with only named exports and no `default` — resolves to `undefined`. `firebase.initializeApp(...)` then throws: TypeError: Cannot read properties of undefined (reading 'initializeApp') crashing startup whenever the Firebase file strategy is configured (`fileStrategy: firebase` or a granular `fileStrategies` entry). Switch to the idiomatic modular named import (`initializeApp`) and use the already-imported `FirebaseApp` type for the return annotation.
…14022) * fix: preserve role SHARE permissions across boot in initializeRoles * chore: sort role method spec imports --------- Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: bound peak memory of concurrent base64 attachment encoding * chore: sort encode imports --------- Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: add .eml (message/rfc822) support to file upload * chore: restore package lock metadata --------- Co-authored-by: Malte Polley <ahabsfriend@posteo.de> Co-authored-by: Danny Avila <danny@librechat.ai>
* test: Guard web search description length * test: Check registered web search description * style: Format web search description assertion
* feat: exclude create_file/edit_file from eager execution Side-effecting host file-authoring tools should not be speculatively eager-executed: a write can land before the turn commits, and the eager path's incrementally-streamed args can diverge from the final tool call, tripping the SDK's 'changed after eager execution' guard so the model is told the write failed and loops (observed with create_file writing a large file to /mnt/data). Pass excludeToolNames so these tools run on the normal ToolNode path with the final args. Requires @librechat/agents with eager-exclusion support; older versions ignore the field. * chore: Bump `@librechat/agents` to v3.2.56 * refactor: reorder imports in run.ts for clarity * fix: also exclude execute_code/bash_tool from eager execution The eager 'changed after eager execution' corruption isn't specific to file authoring — any tool with a large free-form streamed arg is exposed. Observed live: a bash_tool heredoc (a full Python script in `command`) tripped the guard and the write never landed. execute_code (`code`) and bash_tool (`command`) carry large args and run code (side effects), so exclude them from eager alongside create_file/edit_file. * feat: wire codeSessionToolNames so create_file/edit_file share the code sandbox Activates the agents#283 capability: pass create_file/edit_file as codeSessionToolNames so their exec session/files fold into the shared code session and a file they write is visible to later execute_code/bash_tool calls (and the existing session is injected into their requests). No-op until @librechat/agents ships codeSessionToolNames (agents#283). * test: guard code-tool eager/session wiring in createRun Asserts createRun passes excludeToolNames (create_file/edit_file/execute_code/ bash_tool) and codeSessionToolNames (create_file/edit_file) to Run.create — the wiring the create_file->bash_tool sandbox-sharing chain depends on, which was silently missing before. Guards against a future edit dropping it. Mirrors the run-summarization test harness (mocks Run.create). The full create_file->bash_tool chain runs through the real code sandbox and can't run in the mock CI harness; the SDK mechanism is covered by @librechat/agents unit tests, and this guards the LibreChat wiring. * style: fix prettier formatting in run-codeTools test * chore: Bump `@librechat/agents` to v3.2.57
Models sometimes pass edit_file's `edits` as a JSON-encoded string (or stringify individual edit entries) instead of a real array. That failed validation with "Provide old_text and new_text, or a non-empty edits array" and forced a full retry round-trip. normalizeEditArgs now JSON-parses a stringified `edits` value (and stringified entries) before validating. Non-strings and unparseable strings are left untouched, so the existing explicit errors still fire.
…IDs (#14054) * 🧠 fix: Apply Bedrock thinking config to bare inference-profile model IDs The Bedrock request parser gated thinking config, sampling handling, and the anthropic_beta headers on the model ID literally containing `anthropic.`. When a deployment uses an application inference profile, the LibreChat model ID is a bare `claude-*` (e.g. `claude-sonnet-5`) that maps to the profile ARN — so the gate never matched, no `thinking` config was sent, and reasoning models returned empty thinking blocks (most visibly: Claude Sonnet 5 never streamed reasoning, while `us.anthropic.claude-opus-4-8` did). Match on the `claude` family token instead of the `anthropic.` prefix so prefixed (`anthropic.`, `us.`, `global.`) and bare inference-profile IDs are handled identically. Verified e2e against live Bedrock via the agents SDK: a bare `claude-sonnet-5` now sends `{type:'adaptive', display:'summarized'}` and streams reasoning. Non-Claude Bedrock models (llama/cohere) and pre-thinking Claude (3.5 sonnet) are unaffected. * 🧹 fix: Strip stale thinking fields for non-thinking Claude Bedrock IDs Follow-up to the bare-ID matching change: broadening the anthropic guard to match bare `claude-*` meant a non-thinking Claude profile (e.g. a bare `claude-3-5-sonnet` inference profile) took the Claude cleanup branch, which kept persisted `thinking`/`anthropic_beta`/`output_config` from a previously-selected thinking model — leaking unsupported fields after a model switch. Extract `isThinkingModel` and, in the Claude cleanup branch, strip the thinking fields when the model isn't thinking-capable. Also fixes the pre-existing prefixed `anthropic.claude-3-5-sonnet` case (which already kept stale thinking). Thinking-capable models (sonnet-5, 3.7-sonnet) still keep their config. * 🩹 fix: Preserve user anthropic_beta on non-thinking Claude cleanup The non-thinking stale-cleanup deleted amrf.anthropic_beta, but that is the generic Bedrock Anthropic beta field and may carry a user opt-in (e.g. max-tokens-3-5-sonnet-2024-07-15 for extended output on Claude 3.5). Strip only the thinking-specific fields (thinking/thinkingBudget/effort/output_config) and leave anthropic_beta intact. * fix: clear persisted AMRF (output_config, thinking, generated betas) on bare Bedrock profiles * fix: preserve persisted effort on resume + strip stale thinking/betas across bare profiles * fix: normalize string/comma-delimited anthropic_beta before stripping generated betas
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request Template
Summary
Please provide a brief summary of your changes and the related issue. Include any motivation and context that is relevant to your changes. If there are any dependencies necessary for your changes, please list them here.
Change Type
Please delete any irrelevant options.
Testing
Please describe your test process and include instructions so that we can reproduce your test. If there are any important variables for your testing configuration, list them here.
Test Configuration:
Checklist
Please delete any irrelevant options.