[pull] main from danny-avila:main - #269
Merged
Merged
Conversation
The agent tool-resource branch of DELETE /files removed a file's association from the agent and answered 200 without reaching processDeleteRequest, so the bytes on disk and the RAG chunks of a file the caller owned survived the delete. A replaced File Search document kept answering from its old chunks (#15930). An attached file now goes through the full delete pass, but only when nothing else holds it: a file the caller does not own, and a file any other (agent, tool_resource) pair still references, are unlinked and left whole. Nothing is unlinked until the destroy it depends on has succeeded. - packages/api/src/files/deletion.ts: deleteAgentResourceFiles owns the operation with its db methods and delete pass injected. - packages/data-schemas: getSharedResourceFileIds answers the last-reference question, excluding the pair being removed by the agent's _id. - Local/crud.js: unlinkFile propagates non-ENOENT, so a file whose bytes survive is no longer reported as deleted. Co-authored-by: Mohammed Alshyakh <mohmedmm@users.noreply.github.com>
…reads (#16058) Since #15694 bounded each turn's attachments across history, a thread whose history counts more than `fileLimit` model-bound files is refused on every turn, including turns that attach nothing. Two kinds of file reached that count that never belonged in the prompt. Code outputs: priming clears an expired sandbox reference on the turn's copy of the record so the file is re-provisioned, and a route-less record without a reference was classified as prompt content, so it counted toward the limit and could be encoded as media. Code outputs now stay tool-owned regardless of reference liveness, through one predicate shared by admission, BaseClient delivery and the child run-file encoder. Tool-routed spreadsheets: with `textFallbackWithoutTools`, #16027 delivered the fallback text until a tool held a copy. Run Code receives its copy only on its first call, which a refused turn never makes, so the text counted on every later turn. An enabled Run Code is a reader again for the types it can read; File Search still reads only what its store holds.
`getUserPrincipals caching > deduplicates concurrent cache builds for the same member key` fails intermittently in CI, expecting one `cache.set` and receiving two. It failed that way on an unrelated pull request, on a `data-schemas` job whose diff touched nothing in this package. The test starts three concurrent lookups behind a fake cache whose reads resolve after 10ms and whose `get` never returns what `set` stored, then asserts a single build. `getMemberGroupIds` registers its in-flight entry only after the awaited cache read, so the three callers coalesce only while they overlap. When the scheduler serializes them, the first build finishes and clears its entry before a later caller reads, and that caller takes the miss path alone. In production it would find the value the first build wrote; against a fake that always misses it rebuilds and writes again, which is the second `set`. Reproduced deterministically by resolving the first read immediately and later reads after 50ms: expected 1, received 2, exactly as CI reported. The delay is now an arrival barrier that holds each read until all three are inside the miss window, so the test pins the concurrency it describes instead of depending on timing. Reads arriving after the barrier opens pass straight through, so the second read a lock holder performs cannot deadlock. Both concurrency tests share the store-backed fake cache, and a caller that does miss alone now behaves as it would in production. The tests also became stricter. With the in-flight check disabled they fail at three builds rather than two, so a genuine regression no longer looks like the flake it used to produce. Co-authored-by: Lia <lia@librechat.ai>
…6075) When an MCP server's resource endpoint rejects the access tokens its authorization server keeps issuing, a connection being established silently refreshed on every 401 and retried with a token that was rejected again. Once the connect attempts ran out, the failure surfaced as an ordinary connect error, so reinitialize reported `oauthRequired: false`, agent tool loading never showed a sign-in prompt, and agents whose MCP tools all came from that server failed closed. The establishment handler in `MCPConnectionFactory` now remembers the tokens its silent refresh issued. When the server rejects that same credential set again while those tokens are still the stored credential, it starts interactive OAuth instead of refreshing, which issues the authorization URL through `oauthStart` so tool loading prompts for sign-in. A credential another request stored in the meantime, or a different credential set, keeps the existing refresh path.
* 📱 fix: Fit the Tool Library and Skills Pickers to Small Screens The Tool Library dialog assumed a viewport wide enough for its 224px navigation rail: below `md` the rail took more than half of a 390px screen and the card grid was squeezed into the remainder. The rail is now `md`-only, and the same kind and view navigation renders as a horizontally scrollable chip row under the search field. Card and row actions were hover-only, so Configure, Favorite and Remove were unreachable on a touch screen. They stay hidden until hover only where hover exists (`@media (hover: hover)`), and are always visible otherwise. The Skills picker header put the create button, the filter field and the view radio on one row, where the field collapsed to 50px beside a `shrink-0` radio. The view radio and the create button now sit on the title row, the create button flush right, and the filter field owns the row below them. The dialogs fill the viewport below `md` and switch `vh` to `dvh`, so mobile browser chrome no longer cuts off the bottom of the panel. * ♿ fix: Gate Hidden Tool Actions on Touch, Not on Hover Hiding an action until hover has to ask whether a finger can reach it, and `(hover: hover)` does not answer that: on a 2-in-1 it is true because it describes the trackpad, while the touchscreen sits right next to it. Tool cards and tool rows gated their Configure, Favorite and Remove controls on that query, so a finger user of such a device saw no actions at all. `packages/client/tailwind.preset.cjs` already carries `touch` for this exact distinction, `(any-pointer: coarse)` rather than `(pointer: coarse)`. This adds its inverse, `no-touch`, and gates the hidden state on it, so the hide applies only where no coarse pointer exists at all and hover no longer takes part in the decision. * 📐 fix: Split the Skills Header Onto Two Rows Below md The Skills header put the create button, the filter field and the view radio on one row. The radio's segments are `whitespace-nowrap`, so the group never narrows below 261px, and the field — `flex-1`, basis 0 — absorbed the shortfall and measured 50px on a 390px screen. Below md the radio and the create button now take the first line, the create button flush right, and the field takes the second. From md the row is exactly what it was: create button, field, category, radio. One DOM order serves both through `order-*`, so the radio and the create button each exist once. * 🧷 fix: Let the Skills View Radio Wrap Instead of Clipping The three view segments carry `whitespace-nowrap` and `px-4`, so the group has a hard 261px minimum in English and more in longer locales. On a 360px phone that group and the 42px create button exceed the dialog, which is `overflow-hidden`, and the trailing option becomes unreachable. The group now uses the primitive's `wrap` variant and may shrink, so the segments flow onto a second row; at 390px they still occupy one. `wrap` reproduced `inset-y-1` by assuming the group had no vertical padding, so turning it on for a `p-1` group shrank the moving indicator by 8px and left it floating inside its segment. It now measures that padding from the first row's offset, which leaves an unpadded group's geometry unchanged. * 🧪 test: Cover the Small-Screen Tool Library and Skills Header Ten mock-harness scenarios for the behaviour this branch changes, one per acceptance scenario id, asserting rendered geometry and computed styles rather than class strings. The tool library file covers a phone viewport (the dialog fills it, the rail is gone, the chip row filters, and no card crosses the viewport edge — the 744px grid-column overflow), the action gate on all three pointer profiles (visible with a coarse pointer, hover-gated with a mouse, revealed by keyboard focus), the selected tool row, and the unchanged desktop rail. The skills file covers the two-row header below md, the wrapped view radio at 320 and 280 CSS px with every option inside the dialog and the indicator on its checked segment, and the single desktop row. * 🧪 test: Pin the Scenario Contexts and Settle Their Geometry The verifier runs every tagged test in three projects, one of which is a touch phone, so the mouse scenarios asserted a mouse on a device that reports a coarse pointer, and the desktop rail on a 412px screen. They now pin their own viewport and pointer instead of inheriting the project. Geometry and opacity were also read mid-animation: the dialog scales as it opens, which put its left edge at 1.5px rather than 0, and the actions fade over the shared motion duration, which caught keyboard focus at 0.84. Both now poll for the resting value. With nothing selected the skills section offers only the dashed empty-state card, whose accessible name carries the hint line, so the exact-name locator for Add skill never matched. * 📏 fix: Keep the Tool Library Shell and Its Body the Same Height Giving the shell an explicit `md:h-[88vh]` while the body kept its 840px ceiling let the two disagree: on a viewport taller than about 955px the difference became dead space below the catalog, 216px at 1280x1200, and below `md` the full-height sheet left 293px on a 744x1133 tablet. The body carries the height from `md` again, the way it did before this change, so the shell wraps it. Below `md` the shell stays the full-bleed sheet and the body is capped to the same `100dvh`, which it needs because its `h-full` resolves against a grid area sized to the whole catalog. Measured shell and body heights now agree exactly: 840 at 1280x1200, 757 at 1280x860, 1133 at 744x1133 and 844 at 390x844, with the catalog still scrolling inside its own container. * 🎯 fix: Keep the Search Field Clear of the Dialog Close Button `md:px-6` on the search row is a responsive variant, so it is emitted after the base utilities and reset the `pr-12` that reserves room for the close button: on desktop the row's right padding dropped from 48px to 24px and the field ran 16px underneath the button. It now sets only the left side at `md`. Measured gap between the field's right edge and the button: 8px at 1280x860 and at 390x844, with no vertical overlap. Asserted inside the phone-viewport and desktop-rail scenarios rather than as a new id, since it is the same surface. * ⌨️ fix: Read the Skills Header in Tab Order and Keep the Chip Row Draggable Two defects with the same shape: a control that the eye can reach and the keyboard or the mouse cannot. The Skills header used `order-*` to serve both layouts from one DOM order, which left tab order disagreeing with the visual order from `md`: focus went from the rightmost radio group back to the create button and then into the middle. The breakpoint now selects the DOM order instead, the way the marketplace dialog already selects its rail, so each layout reads in the order it renders. Every control is still built once, so there is one radio group and one create button at any width. The mobile chip row hid its scrollbar unconditionally. A narrow desktop window is below `md` as well, and there a wheel scrolls the page rather than the row, so Made by you and Favorites could not be reached with a mouse at all. The hiding is now gated on `touch`, the same `(any-pointer: coarse)` query the rest of this branch uses. Measured: desktop DOM order create, field, radio and mobile radio, create, field, each matching its layout; `scrollbar-width` resolves to `auto` in a 520px mouse window whose row overflows, and to `none` on a touch phone. * 👆 fix: Give the Touch Layout Touch Geometry Two findings with one cause: the coarse-pointer layout inherited the mouse layout's geometry after this branch changed what it shows and in what order. Card actions no longer fade out where a finger can reach them, but they are absolutely positioned over the card's content, so on touch they covered the last line of a three-line description for as long as the card was on screen - an overlap that used to be transient. The card now reserves a strip for them there, `touch:h-36` with `touch:pb-9` on the content, which keeps all three lines. The chip row is that layout's primary navigation and exists only where a finger is what reaches it, yet its chips were about 30px tall and its create control 32px, while the repository already exposes the 44px `theme-control-touch` size for this. Both now carry that floor through `touch:`, and the mouse layout keeps its compact sizing. Measured on a 390x844 touch profile: all eight chips 44px, the card 144px, and the description's box clear of the action cluster. Both are asserted in the phone-viewport and touch-action scenarios.
* 🪶 perf: Render a Finished Message's Markdown in One Pass Opening a conversation rendered every assistant text part per top-level block: a full mdast parse just to find block boundaries, then one ReactMarkdown pipeline, two providers and a layout effect per paragraph, table and fence. The split only pays while a message streams, where it keeps completed blocks memoized. A message that was never streamed in this view now renders as one pipeline and skips the boundary parse, keyed on its source so an in-place edit remounts it with fresh code and artifact indices. A message that streamed here keeps the per-block split after it finishes, so ending an answer never remounts its blocks. The benchmark gains a finished-message mount case. * 🪪 perf: Keep the Agents List From Re-Rendering Every Message Part The author header a message hands its parts, restated after an inline steer, was an element built from the resolved agent. The agents list resolves after first paint, so that element changed identity, broke the memo on ContentParts and re-rendered every part of the message, even though the header only shows after a steer. Both row renderers now hand the parts one constant header element that reads the author from AuthorContext. An agent or assistant resolving re-renders only the headers on screen, and the row's own icon and label reuse the same memoized author. * 🧷 fix: Move a Finished Message to Per-Block Rendering on Its First Change A finished message kept its single pipeline keyed on its source, so every change remounted all of it. An artifact editor saves the message every few hundred milliseconds while the user types, and each save collapsed its expanded Mermaid rows and reset its code blocks, where per-block rendering had re-rendered only the artifact's block. The single pipeline now serves a message only while it is exactly as it mounted. Generating or any content change moves it to per-block rendering for good, which remounts it once; later changes re-render only the blocks they touch. The move happens as generation starts, so the fade baseline is set on the blocks it mounts. The splitter also sliced each top-level block after its indentation, which decides how the block parses: an indented fence under a two-digit list item kept its indentation in the code, and a two-space-indented list turned a note into a code block. Blocks now keep their line's indentation.
* fix(files): read cross-region CloudFront attachments * fix(files): propagate CloudFront stream failures and cancellation
) * 📱 fix: Open Sign-In and External Links From iOS Home-Screen Apps Sign-in prompts, the MCP OAuth Continue buttons and the account menu links opened with window.open and a noopener,noreferrer features string. WebKit treats a features string as a popup-window request, which an iOS home-screen app cannot open, and the chat sign-in also awaited its CSRF bind before opening, outside the tap. Links now open through a target=_blank link with rel=noopener noreferrer, and the chat prompt binds when it appears so the tap opens the provider synchronously. * fix: Keep sign-in closed until its CSRF bind lands, and surface builder OAuth in its dialog A tap while the chat prompt's bind was still in flight could open the provider before its CSRF cookie existed, and an Action callback has no fallback without that cookie; the button now stays disabled until the bind lands. The MCP builder card opened the authorization URL only after its initialize request, which iOS home-screen apps refuse and which left the card with no way to continue; it now shows the MCP OAuth dialog instead. * fix: Let the tapped prompt own the CSRF binding, and show only the card's live OAuth flow Live sign-in prompts share one CSRF cookie per callback path, so the last prompt to bind owned it; a tap now re-binds its own prompt right after opening the provider. The builder card kept a local copy of the authorization URL that outlived its flow and could reopen it on a later initialization; it now shows the manager's shared flow URL, cleared when the flow ends, and only for a flow the card itself started.
* 🌓 refactor: Scrim OGDialog With the Overlay Theme Role OGDialog scrimmed with a literal bg-black/80, so a theme that redefined surface-overlay moved the Dialog and AlertDialog families and left every OGDialog black. The scrim now paints bg-surface-overlay/80: the opacity is unchanged, so the themes whose overlay resolves to black (dark, and both high-contrast modes) render the same rgba as before, and the light theme gets the gray scrim it defines, at 4.3:1 against the dialog it frames. Follow-up from the review of #15798, tracked as berry-13#14. * 🧪 test: Carry the Scenario Theme Mode in the URL The black-overlay scenario switched modes by writing `color-theme` and reloading, but the init script that seeds the mode stays registered for the life of the page and rewrote it on every load, so the high-contrast iterations re-read the first mode and never matched their `<html>` classes. The mode now rides in the URL and the init script copies whatever the current navigation asks for. * 🧪 test: Read the Scrim of the Frontmost Open Dialog The scrim helpers resolved the dialog with `querySelector`, which takes the first match in document order. Nothing in the suite is affected — the settings modal is Headless UI and carries no `data-state`, so the stacked scenario already measured the account confirmation — but a scenario that opened an OGDialog from inside another one would have silently read the outer dialog's scrim. The helpers now resolve the frontmost open dialog by z-index and read its sibling scrim, and the color and both sample points come back from one pass over the DOM. * 🗂️ fix: Let the SharePoint Picker Take the App's Scrim The picker rendered an OGDialogOverlay of its own beside the OGDialogContent that portals one anyway. Both sat at depth 1, so both resolved to z-index 130, and the primitive's overlay — mounted second — painted over the picker's, so the lighter black it asked for never reached the screen. The dialog is opaque, so it needs no backdrop of its own: the extra overlay and its literal are gone and the picker scrims with the theme's overlay role, exactly once, like every other OGDialog. * 🧪 test: Compare the Picker's Scrim to the Primitive's Own The picker spec pinned the scrim's utility classes, which says nothing a reader of the app would recognise and would break the next time the primitive's opacity moves. It now renders a bare OGDialog beside it and asserts the picker's single scrim is the same layer the app paints.
* ⏳ fix: Keep BYOM tools queued across capacity windows * ⏳ fix: Configure the admission budget and refresh queued credentials Addresses the Codex review on #16060: - Add `limits.maxQueueWaitMs` to the code environment config schema so a deployment can shorten or disable the five-minute admission budget from `librechat.yaml`; omission keeps the merged behavior, `0` surfaces the first capacity expiry to the model. Resolved through `resolveAttachedWorkspaceQueueWaitMs` and passed by ToolService, matching how `maxCommandTimeoutMs` already flows. - Accept an auth-header supplier in `executeWorkspaceTool` and mint it per admission attempt, so a call queued past the Code API token TTL presents fresh credentials instead of failing permanently with 401. The attached Bash tool and the workspace file operations now pass suppliers. - Recheck the deadline after a clamped wait and throw the saved rejection rather than opening an admission window that could still be admitted once the budget is spent. * fix: Complete workspace admission wiring and caller regressions * fix: Share protected edit retry budgets and schema limits --------- Co-authored-by: Lia <lia@librechat.ai>
* fix(langfuse): allow text and JSON media uploads * fix(langfuse): support documented attachment MIME types
* chore: restructure pull request template Rewrite the template around the sections reviewers actually need. The header notices become a blockquote, Summary and How it works trade the long prose guidance for short prompts, and the Mermaid, diff, call tree and file tree examples stay as compact format references. Split Testing into a repro description, tested configuration and automated tests, and add Screenshots / recordings and Risk / compatibility sections. Change Type becomes Type of change with refactor, performance, and tests/tooling/CI options, and the checklist drops the items that restate the contributing guide. * chore: prompt for the trigger in the summary section AGENTS.md asks a description to say what breaks, what triggers it and how it behaves after the change, and points at the template for the formats. The rewritten summary prompt dropped the trigger, so a bug fix could be described without the input, state or configuration that reproduces it. * chore: restore the description requirements the rewrite dropped AGENTS.md and CLAUDE.md both state what a description must contain and name this template as where the prompts live, so a prompt this file drops is one an author can omit while following the template. The rewrite lost four: naming the merged pull request behind a regression, the dependency a change needs, picking one or two mechanism views with a sentence each, and showing a whole block where a diff would hide execution order. Also restore the note keeping the Mermaid arrows solid. A dashed arrow spells the HTML comment terminator, which would close the guidance block and print the rest of it in every new description.
* 🫗 fix: Roll Back Incomplete Skill Archive Imports A .zip or .skill import created the skill first and persisted its bundled files one by one, keeping the skill and answering 201 when a file failed validation, a size limit, storage, or its database row. The client never read the internal `_importSummary`, so it showed "Skill created" and navigated to a skill that silently lacked the resources SKILL.md names. Import is now atomic: any failed bundled file rolls back the skill, its file rows, the ACL grant, and the blobs already written to storage, and the request answers 422 `skill_import_incomplete` with the failed paths and reasons. The upload dialog stays open and lists them. * fix: Localize Import Failure Reasons and Report Every Lost File Addresses the first review round on the archive import rollback. Failure reasons are now stable codes (`invalid_path`, `file_too_large`, `archive_too_large`, `archive_entry_changed`, `persistence_failed`) that the client localizes, so a non-English locale no longer sees English text and storage or driver messages stay in the server log. The decompression budget stops the scan mid-archive, and the entries it never reached were missing from the response, telling the user they had seen the whole failure set. Those paths are now reported too. Rollback deletes stored blobs only once `deleteSkill` confirms the skill is gone; otherwise the blobs stay and the request answers 500 `skill_import_rollback_failed`, because a surviving skill pointing at deleted files is worse than unreferenced storage. The failure list scrolls instead of overflowing the dialog, and a response that resolves after the dialog was dismissed no longer repopulates it for the next session. * fix: Ignore Late Skill Import Successes * fix: Make Skill Import Rollback Retryable * fix: Propagate Incomplete Skill Cleanup * fix: Settle Rollback Blob Cleanup * test: Type Partial Cleanup Fixture * fix: Preserve Settled Skill Cleanup Steps * fix: Export Skill Cleanup Step Type * fix: Reconcile Partially Deleted Skills * fix: Reconcile Skill Rollback Caches * style: format cleanup reconciliation changes * fix: complete skill rollback cleanup * chore: leave generated locale catalogs unchanged * fix: preserve synced skill blob cleanup * test: type synced blob cleanup mock * fix: Validate Archive Paths and Bound Rollback Cleanup --------- Co-authored-by: Lia <lia@librechat.ai> Co-authored-by: Danny Avila <danny@librechat.ai>
* 🚐 perf: Enable Redis Stream Coalescing by Default * fix: Preserve Legacy Chunk Frames in Coalesced Publications * fix: Align Coalesced Publication and Append Size Thresholds --------- Co-authored-by: Lia <lia@librechat.ai>
…6056) * fix: Stop Sealing a Code Environment Decision a Chat Never Made Switching an existing chat to a coding agent left the composer unable to send. The chat had no workspace to select, Send stayed disabled, and the workspace chip was a read-only status with a recovery hint. Both layers treated "no decision recorded" as "decided to run without an attached environment". `readPersistedDecision` documented that a stored conversation always carries a decision and inferred `without_attached` whenever `codeEnvironmentMode` and `codeWorkspaces` were both absent, which is the ordinary state of a chat whose turns never involved a code-capable agent. `resolveConversationCodeEnvironmentDecision` then sealed that inference, so the first workspace its owner picked came back as `locked`. The client mirrored the same premise: `locked` was any saved chat, so `useCodeWorkspace` withheld agent defaults, a remembered selection, and a sole workspace, and `CodeWorkspaceMenu` rendered its non-interactive locked branch. Only a recorded decision is sealed now. A conversation holding neither field is undecided, so its first coding turn establishes the decision, and `resolvePersistableCodeEnvironmentDecision` records that decision with its selections; writing the mode alone would leave `attached` without the selections the next turn validates. On the client the same predicate drives `locked`, and the selection defaults that already applied to a new chat now apply to any chat that has not decided. An explicit `without_attached` stays sealed, a legacy row still infers `attached` from the selections it stores, and a sealed decision is still replaced only by its owner's move. * fix: Record the first code environment decision on an existing chat A saved chat that holds no decision may record the one a run establishes, but every caller persists through `saveConvo`, which carries those fields only in `$setOnInsert`. On a row that already exists the write was dropped, so the choice never survived a reload and a later turn could resolve a different workspace. `saveConvo` now fills a whole decision onto a row that holds none, by compare-and-set on that absence: a writer that decided first keeps its decision, a chat that already holds one is untouched, and bare selections never seed a row. * fix: Record the decision a failed turn of a saved chat ran under `saveErrorTurn` built its conversation fields only when it was seeding a new row, so an existing chat kept the stored error turn without the decision that turn ran under: a retry could then pick a different workspace than the failure already recorded. The validated decision now travels on both branches, where the conditional write in `saveConvo` keeps it from replacing a stored one. * refactor: Resolve the error turn's decision fields in `packages/api` The error-turn save derived its own decision fields, putting persistence policy in a CJS controller where `/api` is meant to hold wiring only. It now calls `resolvePersistableCodeEnvironmentDecision`, the resolver the streaming saves already use, so one rule decides what a turn may record: a chat that holds a decision keeps it and receives no decision fields at all. * fix: Keep the legacy code environment lock until the protocol is advertised The composer unlocked a saved chat that holds no decision without consulting `codeEnvironmentDecisionVersion`, so during the rollout window it could attach an agent default and submit it to a replica that still reads a field-less row as a sealed `without_attached` and rejects the turn as `locked`. The unlock now waits for the deployment to advertise the protocol, which makes the hook identical to its previous behavior while the flag is unset. Nothing is lost by waiting: the unmade decision is only reported once that flag is on. --------- Co-authored-by: Lia <lia@librechat.ai>
) Claude accepts base64 documents only for PDF. Through an OpenAI-compatible gateway, a textual file was sent as an OpenAI `file` part with its own media type, which the gateway maps to a base64 document that Claude rejects with a 400 on every later turn. Send it as a text part with the decoded contents, using the same Claude detection as the document filter. Fixes #16054 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ⏳ feat: Negotiate BYOM Background Command Timeouts * ⏱️ fix: Preserve Foreground Programmatic Timeout
* 🧭 fix: Clarify Code Execution Persistence * 🧪 test: Align Code Sandbox Description Contract
* 🗂️ fix: Keep a Running Attachment Chat in the Sidebar Since #15694, a turn with model-bound attachments defers its user-message write until the model admits the payload. That write was also what created a new conversation's row, so for the whole first model call, which with a long generated script or extended thinking can run for minutes, the conversation did not exist. Any conversation-list refetch in that window (starting another chat, deleting one, reloading) dropped the running chat from the sidebar until its run finished, and GET /api/convos/:id answered 404 for it. When attachments are the only reason to defer, the new conversation's row is now written as the run starts, carrying the same fields the message write would set but no message. The message itself stays deferred until admission, and its write waits for a seed still in flight so the two upserts never race. A content policy still holds back every write, the row included, as #14425 intended. The conversation half of saveMessageToDatabase moves into saveConversationToDatabase so the seed and the message write share one path. * 🧱 refactor: Own Turn Conversation Writes in packages/api The conversation half of a turn's writes (the row a message save upserts, the retention context it stamps, and the new seed ahead of a deferred first message) moves into `conversations/save.ts`, which takes `getConvo`/`saveConvo` from the caller. BaseClient keeps only the wiring: the retention lookup that must stay synchronous for Stop, the message save, and the calls into the module. The seed passes an empty `appendMessageIds`, which tells `saveConvo` the row holds no messages yet, so it no longer reads the message list before the upsert. It also resolves the conversation once and leaves it on the request, so the deferred message save reuses it instead of looking the conversation up again. save.spec.ts drives the real data-schemas methods against an in-memory MongoDB: the seed creates an empty row without reading messages, the message save appends to it without a second lookup, an existing chat and a subagent thread are left alone, a temporary chat keeps its retention, and a failed lookup settles instead of rejecting.
* 🔑 fix: Stringify Ban Cache Keys When Redis Is Off Keyv/Mongo throws when checkBan passes a raw ObjectId as the cache key with Redis disabled, which 500s every social OAuth login. Fixes #16025 Signed-off-by: GokayAI <60583610+gokay-ai@users.noreply.github.com> * 🔑 fix: Stringify Violation Log Keys and Cover Bans With Real Keyv logViolation keyed its violation and ban logs by the raw `req.user.id ?? req.user._id`, the same shape checkBan used, so a lean user document would hit the same keyv 5.6 `key.startsWith` TypeError on the namespaced violation stores and record its ban under an ObjectId. The new spec drives checkBan and logViolation through real namespaced Keyv instances (Mongo-backed ban store, in-memory violation logs) with an ObjectId-only OAuth user: an unbanned user passes, and a ban logged from one address is enforced for the same user from another. * 🎯 refactor: Stringify Only the ObjectId Fallback for Ban Keys The id is converted once, where a non-string can enter: the lean document's `_id` fallback. `req.user.id` is already a string on every path that sets it (the JWT strategies, Mongoose's `id` virtual, the 2FA limiter's temp-token id), and the email lookup already stringifies, so `getBanCacheKey` and that line go back to their dev form. --------- Signed-off-by: GokayAI <60583610+gokay-ai@users.noreply.github.com> Co-authored-by: Danny Avila <danny@librechat.ai>
* 🗣️ feat: Trace Viewer Roles, Agent Identity and Tool Rounds * 🧪 test: Stub the Trace Surface in the ChatView Subagent Spec
* 📬 fix: Persist Background Results Before Parent Rows * fix: Harden durable background result delivery * fix: Fence durable background completion ownership * fix: reconcile durable background receipt ownership * test: align background receipt mocks with contracts * fix: Fence durable result ownership and recover interrupted cleanup * fix: Mark independent receipt projections for task-local ownership * fix: Carry receipt provenance through message patch inputs * fix: Preserve resumed receipt delivery during mutual claim yielding * fix: Preserve conversation cleanup for non-receipt owner identities * fix: Preserve receipt recovery across cleanup and retry exhaustion
* 🧺 fix: Coalesce Durable Background Wakeups * fix: Bound replayed background result claims * fix: Harden background result coalescing controls * fix: Reconcile background result claims * fix: close background batch ownership races * fix: Keep completion batching atomic and bound serialized metadata * refactor: Reuse canonical background claim contracts * fix: Expose serialized claim budget in the persistence contract * test: Preserve claimed V1 results across later background batches
* feat: bind attached workspaces to conversations * ⬆️ chore: bump agents to 3.8.8
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )