mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 20:24:21 +00:00
2261 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a70bcf66a4
|
🪢 feat: show Langfuse session link in shared chats (#15273)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* feat: show Langfuse sessions in shared chats * fix: wait for optional auth before loading shares * fix: require Langfuse access for shared session links * refactor: move shared Langfuse policy into API package * fix: wrap shared chat actions on mobile * fix: coordinate shared Langfuse link loading * perf: batch shared Langfuse capability checks |
||
|
|
03fa98eb37
|
🕸️ feat: Complete Keenable Web Search Provider (#15288)
* feat: wire Keenable web-search provider into config, schema, and UI Keenable landed as a search provider in @librechat/agents (#285, shipped in 3.2.58+), but LibreChat did not yet expose it. This adds the config/schema/UI glue so it can be selected, mirroring the existing Tavily provider. - data-provider: add `keenable` to SearchProvider type + SearchProviders enum, keenableApiKey/keenableApiUrl schema fields, and a keenableSearchOptions block (maxResults, site, attributionTitle, timeout). - data-schemas: register keenable in webSearchAuth.providers and default the key/URL placeholders in loadWebSearchConfig. - api/web: pass keenableSearchOptions through to the provider and handle Keenable's keyless model. Unlike other providers it authenticates with no key (the public endpoint), picking up an optional key/URL when set; the URL override is SSRF-preflighted like other user-provided URLs. - client: add Keenable to the provider dropdown with an optional API-key input. - docs: document KEENABLE_API_KEY/KEENABLE_API_URL in .env.example and a webSearch example in librechat.example.yaml. - tests: keyless + keyed auth resolution, config defaults, and schema parsing. * fix: ESLint no-unused-vars and clarify Keenable yaml example - Remove the now-unused RerankerTypes import in data-schemas web.ts (the lint job runs with --max-warnings 0 on changed files, so this latent warning failed CI once the file was touched). - Note in the librechat.example.yaml Keenable stanza that a scraper (and reranker) is still required for web search to load, and include a Firecrawl scraper in the example. * chore: fix import order drift (sort-imports) * feat: add Keenable as a keyless scraper and select it without a pinned provider The Keenable scraper landed in @librechat/agents#337, so wire the scraper category the same way the search provider already is: `scraperProvider: keenable` reads pages through Keenable's public fetch endpoint with no key (a key only lifts rate limits, and the endpoint is overridden with KEENABLE_FETCH_URL). Paired with `rerankerType: none` this makes a fully keyless web-search stack possible for the first time. Also closes the Codex finding on this PR: because none of Keenable's auth fields are required, the generic auth loop skips it whenever it isn't pinned, so a key submitted through the API-key dialog (which cannot pin a provider) left the providers category unauthenticated. Keenable is now selected in that case, gated on one of its values actually being present so installs that configured nothing keep their current behavior. The scraper gets the same fallback, additionally gated on Keenable being the resolved search provider, so it never silently scrapes for another provider. * fix: select the Keenable scraper from a supplied key, not only for Keenable search The API-key dialog submits credentials and cannot pin a provider, so choosing Keenable as the scraper while search stays on Serper/SearXNG/Tavily had no effect: the unpinned-scraper fallback required Keenable to also be the resolved search provider. A supplied Keenable value now triggers it as well, which is the only signal the dialog can send. The fallback still runs only when no keyed scraper authenticated, and with neither trigger the category stays unauthenticated, so a deployment that never configured Keenable is unaffected. Note the fully keyless choice still cannot be expressed through the dialog: Keenable's key is optional, so picking it with no key submits nothing at all. librechat.example.yaml now documents pinning scraperProvider: keenable for that case. * style: Sort Keenable imports * fix: Harden Keenable auth resolution * fix: Preserve Keenable selection intent * fix: Fail closed on invalid web search auth * fix: close keenable auth gaps * style: sort web auth imports * fix: preserve web search selection integrity * fix: isolate web search auth ordering * fix: silence expected credential misses * chore: bump agents sdk * fix: preserve web search preference ownership * fix: forward cleared Keenable endpoint * style: sort web search hook imports * fix: require intent for credential clears --------- Co-authored-by: Ilya Bogin <ilya.bogin@keenable.ai> |
||
|
|
8ba76507ae
|
🧷 fix: Verify Scheduled HITL State Before Pausing (#15284)
Some checks are pending
Publish `librechat-data-provider` to NPM / pack (push) Waiting to run
Publish `librechat-data-provider` to NPM / publish-npm (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: require durable storage for scheduled HITL * fix: verify scheduled HITL checkpoints before pause * fix: narrow scheduled HITL capability checks * fix: honor ask tool filters in schedule preflight * fix: enforce durability across scheduled HITL resumes * fix: scope scheduled HITL admission to active tools * refactor: move scheduled HITL admission to TypeScript * fix: close scheduled HITL admission gaps * fix: bind scheduled pauses to current checkpoint * fix: cap pending actions at binding deadline * fix: reject expired pending actions * fix: guard approval deadlines atomically |
||
|
|
ab2c0d5362 |
📦 chore: bump @librechat/agents to v3.7.5
|
||
|
|
de59da9636
|
🎟️ refactor: Require Credentials for Local Image Access by Default (#15252)
* 🔐 fix: Protect Local Image Access by Default * 🔐 fix: Scope Image Authorization to Active Sessions * 🧹 style: Format Image Authorization Checks * 🛡️ fix: Harden Image Avatar Authorization * 🧭 style: Sort Image Authorization Imports * 🔐 fix: Close Image Authorization Review Gaps * 🧭 fix: Normalize Stored Avatar Base Paths * 🏢 fix: Resolve Tenant Assistant Image Policy * 🛂 fix: Enforce Effective Image Access Policy * 🧹 style: Flatten Assistant Config Selection * 🧷 fix: Preserve Image Access Compatibility * 🪪 fix: Make Image Sessions Revocable * 🏗️ fix: Move Image Session Policy Into API |
||
|
|
ff1784568b
|
📦 chore: bump axios to v1.20.0, @librechat/agents to v3.7.4 (#15285)
* chore: bump axios to v1.20.0 * chore: bump axios in react-query package.json * chore: bump agents sdk to v3.7.4 |
||
|
|
41ba808e6f
|
🧅 feat: Reveal Subagent Thread History and Event Detail on Demand (#15283)
* ✨ feat: Refine Subagent Thread Activity UX
* test: align subagent completion lifecycle
* chore: satisfy subagent panel static checks
* fix: harden subagent activity pagination
* fix: preserve paged subagent history
* fix: surface unrecoverable activity boundaries
* fix: rebase paged activity after live advances
* fix: bound child activity history state
* fix: preserve rebased child history order
* fix: retain unavailable child history boundary
* fix: fence child history cursor generations
* fix: preserve subagent history continuity
* fix: retain live turns during history rebase
* fix: close subagent history edge cases
* fix: harden subagent history phase transitions
|
||
|
|
151dc9e03e
|
📌 fix: Keep Pinned Chats Pinned Through a New Turn (#15230)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* fix: keep pinned chats in the Pinned section after a new turn A chat's conversation state snapshots the sidebar-owned flags when the chat is opened, and the pin mutation never reaches it. Pinning a chat that is already open therefore leaves a stale `pinned: false` on that state, and the SSE handlers write it back over both list caches on the next turn. The chat drops out of Pinned and reappears under Today, since groupConversationsByDate only skips the rows the chats cache still marks pinned. Strip `pinned` and `isShared` from the conversation state before it reaches the caches, and carry the cached row's value forward whenever an updater omits one. The pin mutation still sends `pinned` explicitly, so unpinning a chat removes it from the section exactly as before. The server half of this bug was fixed in #14860, which put `pinned` in excludedKeys so saveMessageToDatabase's unset sweep stops clearing it, but it went in without a test. Cover it here: without that entry the sweep unpins the row on every message, which is what the released builds still do. * fix: preserve pinned flags during uncached upserts * fix: narrow nullable SAML profiles --------- Co-authored-by: Danny Avila <danny@librechat.ai> |
||
|
|
8b1fcc0fc2
|
🌐 fix: Expose Gemini Models to Vertex AI Agents (#15234)
* 🌐 fix: Expose Gemini Models to Vertex AI Agents * ♻️ refactor: Resolve Shared Vertex Model Catalogs * fix: Preserve Exact Vertex Model Catalogs * style: Format Agent Model Selection * test: Preserve Native FS in Stable Diffusion Spec |
||
|
|
0d0d7d05bb
|
🧬 fix: Harden SAML Identity Binding (#15264) | ||
|
|
f0eda61638
|
🧵 feat: Unify Subagent Child Threads (#15261)
* feat: unify subagent child thread rendering * chore: sort subagent UI imports * fix: preserve bounded subagent thread context * fix: render persisted child activity in unified timeline * fix: bound durable subagent activity projections * perf: cap subagent activity source scans * chore: sort subagent thread imports * fix: align completed child messages * fix: bound selected subagent activity reads * fix: bound child activity storage reads * fix: preserve child receipt truncation state * test: preserve projected receipt truncation * test: align child completion e2e * test: isolate Stable Diffusion logger mock |
||
|
|
44edcbe014
|
🧾 feat: Store Durable Event Actor Receipts (#15265)
* 🧾 feat: Store durable event actor receipts * 📊 fix: Scope actor delivery metrics * 🧱 fix: Close durable receipt recovery gaps * fix: serialize actor action admission * fix: retire terminal batch members * fix: close actor terminal recovery races * fix: reclaim lanes after terminal receipts * fix: close actor receipt recovery gaps * fix: preserve ambiguous legacy handling * fix: recover legacy actor outcomes * fix: admit leased actor actions * fix: serialize actor admission recovery * fix: close actor recovery races * fix: token-fence actor admissions * fix: preserve mixed-version actor fences * fix: harden actor rollout and metrics * fix: gate durable actor receipt rollout * fix: enforce base actor receipt rollout * fix: align experimental actor rollout |
||
|
|
2ec222f117
|
🪪 fix: Preserve SAML NameID Bindings (#15254)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
|
||
|
|
10c2e53c27
|
📮 fix: Screen User-Supplied Structured Tool Endpoints (#15253) | ||
|
|
62a55213f0
|
🖇️ fix: Bind Model-Spec Authorization to the Loaded Agent (#15256) | ||
|
|
0c959deb99
|
🧢 fix: Cap Batched HITL Answers Before PII and Moderation (#15257) | ||
|
|
68fc46a055
|
🪢 feat: Resume Bound Event Actors from Checkpoint Forks (#15227)
* feat: resume event actors from checkpoint forks * fix: fence event actor checkpoint uncertainty * fix: satisfy event actor type contracts * fix: make actor reconciliation recoverable * fix: preserve event actor lifecycle transitions * fix: fence event actor lifecycle outcomes * fix: enforce event actor lifecycle ownership * fix: retain event actor settlement proof * 🔒 fix: Retain Event Actor Receipts Through Repair and Bound Their Journal Repair and compensation deleted the reconciliation row they resolved, which was the only durable proof that the invocation had already applied an external action. A delayed duplicate owner could then reacquire the same invocation id and repeat that action. Both resolutions now retire their receipt to `settled` and record how it settled, so the same-id tombstone survives; `history_repaired` and `action_compensated` still force a cold rebuild. Compensation undoes the effect without re-authorizing the delivery, so a legitimate retry must arrive under a new invocation id. A retried repair converges on its own receipt. Bound the journal so a long-lived actor cannot grow its conversation document without limit: a new fence is admitted only when no active lifecycle row exists, so a capped push can evict nothing but the oldest settled receipts. Stop shipping the unbounded source payload on every bound-child continuation. It rode the delivery body regardless of the feature flag while the sibling `fire` body deliberately sends event identity alone, so a large webhook payload could push a previously working delivery past the chat route's body limit. The actor binds an invocation from identity and never builds the prompt from it. Blank the positional token map on warm continuations. It is derived from the full DB history, while a warm run executes on checkpoint-restored state, so its indices address different messages and the pruner never recounts them — misattributing cached counts to the wrong messages in both directions. Keep the replaced-claim exit on its cleanup path when preserving reconciliation fails: the committing CAS already left a blocking row, so the failed status upgrade costs provenance, not safety. * 🧪 test: Pin the Warm Continuation's Map/Summary Asymmetry Give the warm-continuation client test a populated token map and a real cross-run summary so its assertions bite: the positional map must arrive blank (checkpoint-restored state no longer matches DB-derived indices, and the pruner never recounts a populated entry) while `initialSummary` must pass through unchanged — it rides the system tail and summarizes pre-boundary turns that were excluded from the very history the committed checkpoint was built from, so blanking it would silently drop context no warm run can recover. * ⚖️ fix: Honor Compensation in Settlement and Age-Bound the Receipt Journal A compensated receipt still tombstones its invocation id, but its external effect was explicitly undone — the terminal handler nonetheless replayed every settled lifecycle's stored action as authoritative and settled the public outcome as applied, telling an action-aware source the operation stands and suppressing the new-invocation retry compensation requires. The handler now settles a compensated invocation as failed with an explicit compensation error, overriding even fresh applied run evidence from a replayed generation; verified and repaired receipts continue to replay applied. Receipt eviction is now primarily age-based: a stale same-id owner is bounded by time, not by how many newer invocations settle, so the previous count-only slice let a high-rate actor evict a tombstone while its delayed duplicate owner could still wake and repeat the action. Admission prunes only settled receipts older than a retention window that dwarfs every generation, job, and delivery-retry lifetime, and the count cap is demoted to a raised document-size backstop. * 🔀 fix: Serialize Compensation Against Settlement and Never Evict Live Receipts The terminal handler read its lifecycle snapshot, verified history, and then settled the public outcome — so a compensation resolving the same receipt during that window lost: the handler settled applied from its stale snapshot and no retry could ever change the replay-identity-locked outcome. The receipt's status CAS is now the serialization point: verification resolves the receipt BEFORE settling, whichever transition wins determines the public outcome, and a crash between resolve and settle converges through the retained receipt's replay. The verified-replay probe requires the receipt's own resolution, so a compensated receipt can never satisfy a verification retry. This inverts the settle-before-receipt ordering deliberately: that ordering guarded proof that resolution used to delete, and the receipt now retains its full action proof through resolution. The document-size cap is no longer an eviction quota. A receipt inside its retention window is never discarded: when the journal holds a full cap of unexpired receipts, new invocations are refused fail-closed until receipts age out, making duplicate protection and document integrity simultaneous invariants instead of a rate-dependent trade. * 🎓 fix: Keep Skill-Bearing Event Actors on the Legacy Path Skill primes are spliced into the message list directly ahead of the newest message, and a warm continuation forwards only that newest message — so a checkpoint-restored actor would keep serving the prime bodies baked in at its last cold start and never observe an edited or newly attached skill. Until the actor head carries a context fingerprint that forces a cold rebuild when the agent's skill context changes, agents with always-apply or manual skill primes stay on the legacy path, which re-primes fresh bodies every turn: correct on every event, just never warm. * 📜 fix: Gate Fork Mode on the Skills Capability, Not Just Request-Time Primes History-derived re-priming was a third path into the same staleness class: an actor that previously invoked a skill carries no request-time prime arrays, yet primeInvokedSkills re-resolves that skill's current body from history each turn and the warm slice drops the reconstruction — leaving the checkpoint's old body active after edits. The fork gate now keys on the priming hook itself (present exactly when the skills capability is enabled) alongside the request-time arrays, so every skill-body path routes to the legacy rebuild until #15235's context fingerprint restores warm continuation for skill-bearing actors. * 🧾 fix: Capture Applied-Action Proof at Tool Execution, Not After sendMessage The executor read applied-action evidence from the run-step collection the instant sendMessage resolved, but that collection is populated asynchronously — an applied invocation could classify as actionless (runSteps still empty while the tool result already streamed), discarding its fork and stranding the actor cold while the terminal handler later settled the same delivery as applied from the persisted evidence. Authoritative proof is now recorded in graph context the moment the expected tool executes: the request-owned recorder observes the tool-end chain (which ToolNode dispatches synchronously with both input and output) and applies the same fences as run-step evidence — exact tool name with the MCP-suffixed form, the declared argument subset against the execution input, an error-free result, and the background non-execution receipt exclusion. readAppliedAction consults the receipt first; run-step inspection remains the fallback for paths that bypass the tool-end chain. Regression coverage reproduces the observed ordering: the real executor commits the head from the receipt while run steps are empty, warm-continues the next event, and never re-executes the action; recorder fences and the receipt-first controller wiring are covered separately. * 🎯 fix: Supply Execution Arguments to the Tool End Callback The live Vertex + MCP canary exposed a contract mismatch the synthetic fixtures hid: the ON_TOOL_EXECUTE execution path invoked its tool end callback with output only, while the action recorder must verify the declared argument subset against the execution input. The receipt never qualified, every turn fell back to cold history rebuilds, and the tournament advanced with zero actor heads and zero retained checkpoints while looking successful. The execution handler owns both halves at the same moment, so the fix is at the source rather than a correlation store: ToolEndCallbackData gains the executed call's input and every callback site passes tc.args. A handler-level regression drives the real createToolExecuteHandler and asserts the callback receives both fields; recorder regressions pin the production shapes — an output-only tool end must starve an argument-fenced receipt rather than trust an unfenced match, and still qualifies a name-only expected action. * 🕵️ fix: Mark Background Deliveries So They Cannot Impersonate Applied Actions The background-claim callback reports the ORIGINAL tool's name for artifact attribution on the poll turn that harvests a completed task. A name-only expected action could therefore be impersonated by work some earlier turn dispatched: the recorder would attribute that delivery to the current invocation and commit a head whose state never contained the invocation's own action. The run-step evidence path never had this hole — it sees the poll tool's name — so the recorder must match its provenance discipline. Delivery callbacks now carry an explicit backgroundDelivery marker set at the one site that rewrites the name, and the recorder ignores marked deliveries outright. Regressions pin both halves of the contract: the delivery callback must carry the marker with the poll call's arguments, and a marked delivery can never qualify even a name-only expected action. * 🧿 fix: Version Invalidations, Keep Evidence Ahead of Output Policy, Gate Detachable Actions Three closeout-round findings, each converted into an invariant. Every legacy-path invalidation now advances a durable epoch — including for headless and already cold-marked actors, where the marker alone leaves no CAS-visible trace — and the actor-head CAS requires the epoch observed at preparation. A concurrently prepared fork whose history predates an intervening legacy turn can no longer commit past it; the commit reports an ordinary conflict and journals. Execution identity is now emitted before post-execution output policy: when a side-effecting tool succeeds but its returned content is withheld by the output filter, the callback delivers an outputFiltered receipt with blank content — the recorder accepts it as proof (rejecting model-detached calls it cannot distinguish through the blank shape), the artifact path never sees it, and an applied action is no longer reclassified actionless and re-executed on retry. Background-capable expected actions stay off the fork path: dispatch returns a launch handle every evidence fence correctly rejects, and the completion is provenance-marked as another turn's work, so a fork would settle actionless before the external effect lands with nothing to stop a retry from dispatching it again. The gate mirrors the MCP-suffix name matching of the evidence path. * 🚧 fix: Seal the Whole Legacy Turn Behind a Second Epoch Advance The epoch fenced only the legacy turn's start: a fork preparing after the begin invalidation but before the turn's message persistence observed the new epoch and cold marker, rebuilt from history that did not yet contain the turn, and committed cleanly because nothing advanced the epoch again — making the incomplete rebuild authoritative and clearing the marker. Every legacy event turn now seals its invalidation at terminal persistence with a second epoch advance, on the success, replaced-claim, and error exits alike. Sealing deliberately carries no quiescence requirement — it must succeed while a fork fence is active, because that is exactly the mid-turn race it defeats — and a fork that already committed against the begin epoch is healed the same way: the seal re-marks the head cold, so the next event rebuilds with complete history. Seal failure never diverts the turn's own exit; the begin bump still fences everything prepared before the turn. * 🔗 fix: Replace the Best-Effort Epoch Bump With a Durable Legacy-Turn Fence The second epoch advance could not make a legacy turn atomic, and three findings shared that root cause: two conditional updates left a headless gap a fork could create the head inside; the error exit sealed before saveErrorTurn made the error history durable; and any crash or failure between persistence and sealing left an incomplete fork authoritative, because the seal was best-effort and its failure was swallowed. A legacy turn now carries one durable fence. A token is written before execution by a single update-pipeline write — no two-write gap, and the cold marker is applied only where a head exists via $cond/$$REMOVE. While the token is present no fork may prepare (the adapter refuses) or commit (the CAS requires its absence), because the turn's messages are not yet durable. One atomic write clears the exact token and advances the epoch once history is persisted — after saveErrorTurn on the error exit — and success, replacement, and error exits all route through it. Failure is now fail-closed rather than silent: a failed seal leaves the token set, which keeps blocking forks and is logged as such, and a fence abandoned by a crashed turn is reclaimed only once stale, advancing the epoch and marking any head cold so the next event rebuilds from whatever history actually survived. * fix: serialize legacy event actor turns * fix: close legacy actor fence ownership gaps * fix: preserve legacy actor persistence fences |
||
|
|
f34a49007d
|
🔣 fix: Escape SPA Language Attribute (#15248) | ||
|
|
0383030817
|
⚡ perf: Avoid Parallel Full and Paged Prompt Loading on Startup (#15031)
Co-authored-by: Danny Avila <danny@librechat.ai> |
||
|
|
124e357cbf
|
✏️ feat: Edit Pasted Text and Clear It on New Chat (#15017)
* fix: stop an unsent paste from following every new chat An explicit new chat now drops the unsaved-chat draft key before the composer resets. `newConversation` empties the composer, but the key outlives it and `useAutoSave` restores from that key on the way in, so a long paste that was never sent came back as an attachment on every later new chat. Per-conversation drafts are untouched. Clicking a pasted-text chip opens the text in an editor so it can be corrected before sending, and the chip's subtitle offers returning the paste to the composer. The text comes from the in-memory blob, falling back to the `text` field the file record already carries, so neither needs a new endpoint. `FileContainer` grows a `subtitleAction` prop for the second control. Supplying it swaps the chip's own button wrapper for a full-bleed one behind the content, since a button inside a button is invalid markup and browsers drop the inner one's events. * chore: sort imports to fix static checks * fix: address paste edit review findings - Keep the original paste attached until the replacement upload succeeds, so a rejected or failed save cannot destroy the only copy - Guard edits and queued replacements against conversation switches and new-chat resets, mirroring the long-paste lifecycle guards - Recover text for restored pastes by downloading the stored bytes, which Assistants and agent uploads persist without a text field - Delete uploaded attachments when an explicit new chat discards the draft that referenced them, instead of orphaning the records - Mark paste provenance explicitly (session registry plus files draft) instead of inferring it from a filename a deliberate upload can share - Keep the subtitle action revealed on devices without hover * fix: scope draft cleanup to its tab and delete restored pastes - Stamp unsaved-chat files drafts with the writing tab's session id and skip deletion when another tab owns the record, so a new chat in one tab cannot discard the uploads attached in another - Delete a restored paste's upload when an edit replaces it or returns it to the composer, which the attached flag otherwise preserved * fix: harden paste edit lifecycle guards - Re-check the originating composer and the file map before detaching an edited original on upload success, so navigation or a send during the request cannot remove or delete a file the old draft or sent message still references - Record the replacement upload's paste provenance in the session registry and the files draft, keeping Edit and Move back on the new chip - Bind move-inline to the unsaved-chat token as well, and abort the move when the chip is no longer attached to an unsent composer - Restrict new-chat draft cleanup to ids the composer still owns: library re-attaches and ids with unknowable ownership are spared, and uploads still in flight are deleted once their records reach the files cache, unless the file came back attached in the meantime * fix: match restored paste identities and recheck before opening the editor - Treat a chip as attached when any of its ids (map key, file id, temp id) matches the composer map, since draft restoration keys entries by their temporary upload id while the value carries the server-assigned one; the previous key-only check made Move back silently do nothing and left both chips attached after an edit - Recheck the originating composer and the attachment map after the text resolve before opening the editor, so a send during the download cannot stage a replacement upload into the emptied composer - Extend the provenance predicate to temp ids for the same restored shape * fix: spare re-attached sent pastes and discard stale editor resolves - Force-delete a restored paste only when the composer's own draft claims its id, so a paste that was already sent and re-attached from the library keeps its shared record through Edit and Move back - Sequence editor-open requests so a slow text resolve cannot overwrite the chip a later click selected * fix: carry deferred discards across resets and clear the pending draft - Merge newly deferred upload ids with the pending set instead of replacing it, so a second reset cannot orphan an earlier in-flight upload's eventual record - Match deferred ids against temp_file_id as well, since the files cache keys records by the server id while the discard tracked the request uuid - Clear the pane's pending draft key on an explicit new chat, or a running response's queued text and attachments come back with the next run * fix: mint a fresh tab id when sessionStorage was inherited Duplicated and opener-created tabs start with a copy of the original's sessionStorage, so a stored tab id only proves continuity when the document is a reload of the same tab. Every other entry into a document now mints a fresh id, keeping an inherited one from attributing another tab's live drafts to this composer. * fix: gate restored-paste deletion on draft tab ownership and unclip the chip focus ring - Stamp every files draft with the writing tab, not just unsaved-chat ones, and require the stamp to match before a restored paste's record is deleted, so another tab restoring the same draft is not destroyed - Draw the full-chip Edit control's focus indicator as an inset ring with the surface's radius, since the offset ring was clipped away by the surface's overflow-hidden * fix: resolve paste ownership before restoration and across draft migration - Treat a draft's own pastedTextIds as composer-owned at discard time, so a reload-then-new-chat click deletes or defers those uploads before the composer map has been rebuilt, instead of skipping them - Read both the pending and idle draft keys when claiming a restored paste for deletion, since a response finishing mid-edit migrates the record between them * fix: keep failed edits recoverable and retry failed draft deletions - Reopen the paste editor with the user's corrections when the replacement upload is rejected or fails, instead of leaving only the original's text to reopen - Retain deferred and immediate discard ids when the delete request fails, and retry them on the next files-cache update, so an offline or transient failure cannot orphan the uploads * fix: retry restored-paste deletions that fail A failed delete of a detached restored paste retains its payload in a session store, and the discard retry effect drains retained payloads alongside its own batch on every files-cache update, so an offline or transient failure cannot orphan the upload once its chip is gone * fix: queue failed edits and lock chips with actions in flight - Queue a failed edit behind whatever dialog is open instead of dropping its corrections, and reopen it when that dialog closes - Track an in-flight action per source paste and hide its Edit and Move back affordances until the replacement uploads or the move settles, so the same original cannot be acted on twice * fix: address PR review bot findings Codex: - Keep the tab id on back-forward restoration - Preserve the original tab owner when rewriting drafts - Skip clearing idle and pending drafts another tab still owns - Delete pending-draft uploads before clearing them - Return failedFileIds from DELETE /files and retry those records - Spare reattached files from retained deletion retries - Trigger retained-deletion retries when a delete is retained - Persist deferred discards across reload - Delete embedded owned uploads with a discarded draft - Ignore stale paste-editor failures before toasting - Abort a queued edit after the original is sent - Serialize Move back with a synchronous in-flight lock - Prune paste provenance ids that left the draft * fix: retain paste deletions the server reports as failed The delete route answers 200 with `failedFileIds` when a record's storage delete fails, so the detach path's `.catch()` never fired and the orphaned upload lost its only cleanup reference once the draft provenance was pruned. Inspect the resolved response and retain the deletion when it names the file. Extract the `failedFileIds` reader `useNewChat` already had into the file utils so both deletion paths read the response the same way, and give the paste editor coverage for the failed and accepted responses. Also add the missing `size` on a composer file literal that was failing the client type-check. * fix: release draft claims when their tab is gone and keep cleanup durable A tab stamped its id on a files draft and nothing ever took it off, so a draft saved in a tab the user then closed became unreachable for good: no other tab would restore it, write to it, or clean it up, and the closed tab's id can never be presented again. Tabs now report themselves in a small liveness registry and release the claim on pagehide, and a claim whose tab is no longer around is treated as free. Writers restamp a dead claim rather than preserving it. Ownership also only existed once something was attached, so a typed-but-unattached draft on a shared composer key read as nobody's and another tab's New Chat cleared it. Saving text to one of those keys now claims it the same way. Two more from the same review: - The delete route answers a partial failure as 200, so treating an id as still present unless the response reports it deleted kept a ghost row for a file another tab had already removed. Read it the other way around: only a reported failure keeps a record cached. - A retained deletion whose retry failed again moved no effect dependency, so it was never attempted a second time, and the payload only lived in memory. It is now persisted for the session and asks for a backed-off retry, plus one on regaining connectivity. * fix: keep bfcache claims, retain failed deletes, and move the delete contract to TS Four findings from the latest review round: - pagehide fires with persisted: true when a document enters the back-forward cache rather than closing. Releasing the tab's claim there let another tab take the draft and delete files the restorable document still had attached, so the claim is now only handed back on a real unload; a bfcached tab that is never restored still ages out through the liveness window. - useFileDeletion issued its batch and never looked at the outcome, so a fresh paste whose delete failed was orphaned with no retry. It now retains whatever the server did not delete, reading failedFileIds as well as the rejection. - A partial failure answers 200, so the unconditional success toast told the user a file was deleted while it was still on disk and back in their list. - The delete response contract lived in the legacy JS route. It moves to packages/api as buildDeleteFilesResponse, leaving the route a thin caller. The useFileDeletion spec's mutateAsync mock returned undefined; react-query always hands back a promise, so it now resolves like the real one. * fix: park bfcached tab claims and drop ownership left by an emptied draft A document in the back-forward cache has a frozen heartbeat, so the ordinary liveness window expired its claim after 150s even though it could still be restored with those attachments on screen, letting another tab take the draft and delete the files underneath it. Entering the cache now parks the tab as suspended, which holds the claim for 30 minutes: comfortably past the point a browser keeps a bfcache entry, and still bounded, since a claim that never expires is what stranded drafts under owners that no longer existed. Restoring the document beats normally again and clears the flag. The registry entry grew a shape for this and still reads records written as a bare timestamp. Clearing the text of a shared composer key also left the ownership-only record behind, locking the key to a tab with nothing in it: the next tab to type there could neither restore its own draft nor take the key back. That claim is now released when the text goes and nothing is attached. * fix: keep unlinks out of the delete retry and claim shared text before writing Four findings from the latest round: - Retaining a failed agent or assistant unlink sent it through the generic retry, which replays files alone. That drops the tool_resource context, so the route would take its ordinary delete branch and destroy a record the agent and other references still point at. A failed unlink orphans nothing, so those deletions are simply not queued. - A retry that resolved naming files in failedFileIds left both stores untouched, so nothing moved the effect that would try again. It now asks for another attempt on a reported failure, the same as on a rejection. - The reattachment guard read only the idle new-chat key. After a reload the composer map is empty until the autosave restore renders, so a file the user had reattached to the conversation they were viewing, or to the pending key, could be deleted underneath them. All three keys are checked now, including their paste provenance. - Text was written to a shared composer key before ownership was resolved, so a tab could overwrite another's saved text and still be refused the claim, leaving it unable to restore what it had just typed. The claim is taken first, and a claim with no attachment behind it follows whoever's text is actually stored; one backed by an attachment stays with its open owner. * fix: merge shared discard state and keep restored file-search pastes retrievable Four findings from the latest round: - Every mount of useNewChat (header, sidebar, mobile bar, shortcuts) kept its own snapshot of the pending-discard list and wrote it back over one shared session store, so an id recorded by one instance was dropped by the next write from another, orphaning the upload it pointed at. An update now only resolves the ids that instance knows about and carries the rest through. - Refusing an attachment-backed claim still let the text write land, destroying the owning tab's text for a tab that could not have restored it anyway. The claim now reports whether it succeeded and the write is dropped with it. - A restored paste has no tool_resource on its record, so an edit to one that had been uploaded for file search was re-uploaded as a plain context file and the vector-backed original detached, dropping it out of retrieval. embedded does survive on the record and is only set for a vectorized file, so it is what the destination falls back to. - The reattachment guard collected map keys and server ids but not temp_file_id, while the retry lookup resolves that alias: reattaching a file whose discard was pending under its temporary id would not have protected it. * fix: stop the draft owner refusing its own writes and guard shared pending keys Three findings, the first a regression from the previous commit: - The attachment-backed refusal was evaluated before the owner check, so the tab that owned the draft was refused its own key: once anything was attached, nothing typed after it was saved. Ownership is settled first now, and the refusal applies only to another live tab. - A long paste wrote its provenance and pending-paste record into the shared composer key without checking who owned it, and setFilesDraft preserves the existing owner rather than rejecting the write, so the paste was recorded into another tab's draft, which could then restore and delete the upload while this tab still showed the chip. Both write sites now check first. - Two concurrent runs share the default pending key, and the migration to the new conversation ran before the ownership check: the finishing run moved the other tab's text and attachments under its own conversation and left that tab nothing to carry over. Ownership of the source is verified before migrating, and this composer's own text is still saved either way. * fix: protect cross-tab reattachments and orphaned pastes on every discard path Five findings from the latest round: - The retry guard only read this pane's own draft keys, so a file a second tab had reattached to a conversation this pane never opened was deleted anyway. Drafts live in localStorage and are readable from every tab, so the guard now sweeps every persisted files draft rather than three known keys. - Clearing the composer removed the shared text record without checking who owned it, so an empty composer in one tab erased text another tab was still holding behind its attachments. The clear path takes the same guard as the write path, and both now share one ownership predicate. - When another tab owned the pending key, this tab's own queued attachments were cleared from the map and never written anywhere, because the autosave that would have persisted them had been refused that key for the whole run. They are now written under the conversation the run just became. - A draft write that storage refuses (private mode, quota) left a generated paste with no record to discard it by. New Chat now also collects the live marked pastes the composer is still showing, skipping re-attached ones. - With draft saving off, the reset path deleted files without awaiting or reading the response, so a failure orphaned the upload. It retains what the server did not delete, like every other deletion path. * fix: keep reloading tabs live and spare pastes an active run is using Three findings: - pagehide cannot tell a reload from a close, and the tab id survives a reload on purpose, so releasing the claim there handed this tab's own draft to another one while the document was still bootstrapping. A closing tab is left to the ordinary liveness window instead, which is what the window is for. Entering the back-forward cache is still marked, since that heartbeat freezes. - The text-ownership guard only covered the shared composer keys, but a conversation key is reachable from every tab viewing that chat and is stamped the same way, so one tab could overwrite text another was holding behind its attachments. The guard now applies to any key; the ownership stub is still only created for the shared keys, which tabs otherwise share freely. - Submitting empties the file map but leaves the draft's paste provenance until the final SSE event, so New Chat during a streaming response treated the empty composer as still owning what the message had just sent and deleted files the message, and the run reading them, still referenced. The provenance promotion is skipped while a run is in flight. * fix: give each tab its own presence record and publish live attachments Four findings: - Tab presence lived in one shared localStorage map, so two tabs beating at the same time read the same snapshot and wrote back rival copies; the loser disappeared until its next beat, long enough for another tab to treat its live draft as abandoned. Each tab now writes only its own key, and expired records are swept while reading. - With draft saving off nothing is written to a draft at all, so a file reattached in another tab was invisible to a retry running here and could be deleted underneath it. A tab now publishes what its composers are holding into its own presence record, and cleanup unions that with the drafted ids. - The record written when another tab owns the pending key kept only attachment ids, so a restored chip stopped being recognised as a paste and lost editing and cleanup. Provenance is rebuilt from the session registry. The unsent paste text cannot come along: this tab was refused that key all run, so it was never stored anywhere to carry. - New Chat with draft saving off skipped every embedded record, leaving an unsent file-search paste with its metadata, storage and vectors intact. A paste this composer owns is now included with its real embedded value, while other embedded files are still left alone. * fix: elect one cleanup worker, scope the queue to its account, guard edit writes Three findings: - Every mounted useNewChat (header, sidebar, mobile bar, shortcuts) entered the cleanup effect against one shared store, so a single retry issued the same DELETE several times and toasted about each. A pass is now claimed before it runs; an instance that is turned away asks for a later one rather than dropping the work. - The retained queue outlived a sign-out, so the next account retried the first one's payloads, was refused by the ownership check, and rescheduled forever. Logging out clears the queue and cancels the pending retry. - The paste path checks draft ownership before recording provenance, but the edit path did not, so a replacement could be written into a record another open tab owns, which that tab could then delete while this one still showed the chip. It takes the same check. * fix: match paste identities everywhere and stop migrations clobbering a foreign draft Five findings: - The presence record published only composer map keys, but a restored upload is keyed by its temporary id while the value carries the server one, and a retained deletion in another tab names whichever it recorded. All three identities are published now, matching the local guard. - Migrating a finished run checked that the pending record was ours but not the destination, so a conversation draft another tab owned with attachments on screen was overwritten and restamped. Both ends are checked, and the non-owner fallback no longer writes over a foreign destination either. - The live-paste fallback matched the registry against file_id alone, so a completed paste, marked under its client upload id, read as somebody else's file and its upload survived New Chat. It matches every identity now. - An upload still in flight has no filepath or source, so no discard path can build a payload and the reset drops the chip anyway. Its id is deferred so the record is deleted when it arrives, with draft saving on or off. - An edited paste that had been staged into the code sandbox was re-uploaded as a plain context file, since only the file-search case was reconstructed. metadata.codeEnvRef is durable and now routes it back to execute_code. * fix: silence background cleanup and keep cross-tab protection past a send Four findings: - The reset path matched the paste registry on file_id alone, the same alias gap already fixed in New Chat, so a completed embedded paste read as somebody else's file and survived with its vectors. It matches every identity now. - The background cleanup pass used the ordinary delete mutation, so a storage failure that kept failing announced itself on every retry, and success arrived minutes after the action behind it. The mutation takes a silent option and the retry pass uses it; direct user actions still report. - Each hook instance loaded the pending-discard list once and was never told when another instance wrote it, so work deferred by an instance that then unmounted stalled. Writes now notify every mounted instance, which re-read and apply only a real change. - Cross-tab protection sampled only what a composer was holding right then, and sending clears both the map and the draft, so a file reattached in another tab and then sent could be deleted between retries. A tab now remembers what it recently held for ten minutes, which is long enough for the other tab's next pass to see it and cancel that deletion for good. * fix: honour draft ownership in every clear and track what a message consumed Five findings: - The ownership contract was only applied at the new call sites; the SSE final event, the steering handoff and the debounced text clear still erased records through clearAllDrafts and clearDraft. The check moved inside those helpers, so every path that clears a draft respects it. - Only the explicit logout cleared the retained queue, leaving a silent refresh that returns nothing and a failed user query to carry it into the next account. It clears wherever the session is lost instead, in the one place all three paths pass through. - Using isSubmitting to decide whether a paste was consumed was wrong for a stopped or errored turn: those clear the flag without clearing the draft, so New Chat afterwards deleted files the turn already referenced. Submission now records the ids it took, and those are excluded by name. - The presence sweep only ran from deletion cleanup, so a profile that never had a failed delete accumulated a record per tab until the origin quota ran out and draft writes began failing silently. The heartbeat sweeps. - When a run finished into a conversation another tab owned, this tab's own queued text and attachments were dropped for want of a writable destination. They stay on the key it does own and are restored from there. * fix: mint a tab id when the browser has no randomUUID crypto.randomUUID is absent on insecure origins and in older webviews, and the throw left the tab with an empty identity: every draft was then written without an owner and every ownership guard read another tab's record as its own, reinstating exactly the loss this layer exists to prevent. Falls back to getRandomValues, then to a local mint. The id only has to tell tabs apart. * fix: address PR review bot findings Clear the retained deletion queue on every direct authentication exit, not just the debounced context update: an empty or rejected silent refresh, a failed user query, and the external-IdP logout all leave the page without passing through setUserContext, so the queue survived into whoever signed in next and retried under credentials the ownership check rejects forever. Settle the edit lock when a replacement upload is aborted. Removing the replacement chip mid-upload consumes the lifecycle through onAbort, which the paste editor never handled, so the source paste kept its Edit and Move-back actions hidden for the rest of the session and the typed correction was lost. Keep the temporary-file cleanup payload for whatever the server reports as failed. The delete route answers a partial storage failure with a 200 carrying failedFileIds, and the cleanup mutation cleared FILES_TO_DELETE wholesale on any success, dropping the only automatic retry those orphans had. Persist both paste registries per tab. They lived in module-level sets, so a reload kept the files draft but forgot the paste had been consumed, and New Chat then classified an already-sent paste as unsent and deleted a file the persisted message still references. Withdraw discarded ids from tab presence. A removed, moved, or discarded chip kept its recent entry for the whole window, and the retry sweep read that as evidence the file had been reattached: it cancelled its own cleanup and left the failed upload orphaned on the server. Presence records whose heartbeat cannot be read are skipped rather than rewritten, since giving one a fresh seenAt would revive a dead tab's claims over every id it still held. * fix: address second round of PR review bot findings Stop a settled deletion from undoing the logout clear. A DELETE that was already in flight when the session ended settles afterwards, and its handler is the last reference to that payload, so it wrote the departing account's records straight back into session storage. Clearing now latches retention shut and only a newly established session reopens it, which also covers the paste editor's own retention and the discard paths, not just this one writer. Reinsert a failed paste when nothing durable holds it. A composer the user has typed into is deliberately left alone while a recovery record exists, because that record restores at an anchored offset later. When the shared draft key belongs to another live tab the guard skips the record entirely, so the upload callback held the only copy and refusing dropped the text outright. It now goes back in at the offset its anchors resolve to, which is where a restore from a record would have put it. * fix: address third round of PR review bot findings Withdraw attachment presence from this tab only. The sweep cleared the withdrawn ids out of every tab's recent map, which is the one record a second tab has left once it has reattached a file and sent it: its composer and its draft are both empty by then, so erasing that entry handed the next retry a file it read as abandoned and let it delete the upload out of the message now referencing it. The withdrawing tab always published what it withdraws, so its own record is all it needs to touch. Guard the direct New Chat deletion against other tabs. The retry effect consults every other tab's drafts and published presence before deleting, but the discard that runs on New Chat went straight to the request, so it raced past that guard and could delete a file another tab still had attached or had already sent. It now consults the same two sources, excluding its own draft keys and its own presence record, which hold exactly what the discard is throwing away. Fix the import order in Presentation.tsx, which CI static checks flagged. * fix: address fourth round of PR review bot findings Read this tab's presence before sweeping stale keys. Timers pause while the machine sleeps, so a live tab can beat again with its own record already past the liveness window; the sweep reaped it and the write that followed published an empty presence, and nothing republished it because the file map had not changed. Another tab's retry then saw no claim on chips this one still had on screen. Keep submitted-use evidence when a later chip is withdrawn. The same file can be sent on one message and reattached afterwards, and once the composer and draft have cleared, its recent entry is the only cross-tab record that a message still references it. Withdrawing a chip no longer erases an entry for an id a submission already consumed; it ages out on the ordinary window. Clear composer drafts when the account changes. A files draft carries the whole text of a paste held as a file, and the browser tab keeps its identity across an in-app account switch, so the ordinary draft restore could hand the next account the previous one's writing. Both draft families are now dropped on the sign-in and sign-out paths, ahead of the skipFirst exception. Spare a submitted paste from the edit path's explicit deletion. Editing or moving a reattached library file that an earlier message sent deleted the server record underneath that message, because the draft-ownership check succeeded and nothing consulted the submitted marker. Validate an edited paste as a replacement rather than an extra file. The original is deliberately still attached while the replacement uploads, so the shared validation counted both and rejected the edit at the file-count or total-size limit; with a limit of one, a lone paste could never be edited. Preserve failed rows after a table deletion. The table's own cache update removed every requested file without consulting failedFileIds, undoing the partial-aware update and hiding a file whose storage delete had failed. Document the two deliberate dependency omissions in AuthContext, which CI now lints at zero warnings because the file is part of this change. * fix: address fifth round of PR review bot findings Clear composer drafts on the way out of a session, not only on the way in. Clearing them from the login mutation missed social sign-in entirely: OAuth, OpenID and SAML leave through direct links and come back through the silent refresh, so a different account could arrive in the same tab with the previous account's drafts and tab identity intact and have its paste text restored. The draft clearing is now paired with the retained-deletion clearing in one helper used by every authentication exit, so neither can be wired into a path the other was missed from. Rebuild paste provenance when restoring a queued upload. A paste queued during a run has its pending draft taken by takeComposerDraft, so choosing Edit message restored the upload into an empty composer with nothing recording that it was a generated paste. Filtering existing provenance could not recover that, and an unmarked restored chip is treated as a shared attachment: removing it would not delete it and New Chat skipped it, orphaning the unsent upload. The session registry still knows, so it is consulted. Drop paste provenance when a rejected upload is removed. Validation can reject a paste before it reaches composer state, and the failure path removes it with removeFile, but the id stayed in pastedTextIds. That left a record hasDraftAttachments reads as a real attachment claim with no chip behind it, and with the file map unchanged nothing pruned it, so it locked every other tab out of the shared composer key. * fix: address sixth round of PR review bot findings Centralise the foreign-claim guard. Every path that deletes an upload has to ask whether another tab or pane still claims the file, and the guard was being assembled by hand at each site, which is exactly why it was missing from three of them. collectForeignAttachmentClaims now builds that set once, and the New Chat discard, the no-draft reset fallback in useNewConvo, and the paste editor's explicit deletion all consult it. A record another tab claims is skipped rather than retained, since it was never this pane's to delete. Scope presence withdrawal to the pane that owns it. One tab holds several composers and the presence record is flat, so the hook that won the global deletion pass swept every pane's entry while knowing only its own file map, erasing the evidence of a chip a sibling pane still had on screen. Withdrawal now takes the pane index, and an id another pane still lists keeps its recent entry too. Guard destructive draft clearing against text-only claims. claimComposerDraftTab stamps a key that holds nothing but text, and the write guard ignores a claim with no attachment behind it, so a tab finishing a run that began as an unsaved chat cleared the shared new-chat key and took another tab's half-written message with it. Clearing now honours any live foreign stamp, while text writes keep their deliberate last-writer-wins behaviour. Mark queued override files as submitted. A during-run queued message drains through overrideFiles into the reuseFiles branch and skipped the marker loop entirely, so reattaching that paste later left isPasteSubmitted false and New Chat or an edit could delete a file the queued message still referenced. * fix: address seventh round of PR review bot findings Treat publishing an attachment as proof of liveness. The publisher carried the old seenAt over, so a tab whose timers had been paused past the liveness window published a chip and stayed expired until its next interval tick, long enough for another tab's cleanup to sweep the record and delete the file under the chip that had just appeared. Count a sibling pane as a claim. The foreign-claim helper excluded this tab entirely, so the pane doing the discarding could not see the other composer in the same tab and deleted a file it still had on screen. Live claims are now gathered per pane: only the discarding pane's own entry is left out, along with this tab's recent map, which is flat and cannot say which pane an id came from. Keep a tab identity when session storage is unusable. It can be blocked or full while localStorage still works, and returning an empty id left the document unattributed, which every ownership and liveness guard reads as no owner, so tabs could destructively clear each other's attachment-backed drafts. An id that lives only for this document still tells the open tabs apart. Scope attachment withdrawal in the deletion hook to the originating pane, and thread the composer index through ChatForm, FileFormChat and FileRow to supply it. Removing a file from one side-by-side composer withdrew the id for every pane, and with drafts off the sibling never republished its claim. Check foreign claims before deleting a live edit source. The guard only covered the restored path, because detach returns early for an in-memory upload before reaching it, so editing a live paste another tab had reattached from the library deleted the file underneath that tab's chip. Keep autosaving to the pending key while the destination is not writable. The preserved queued work was written under the pending key but the destination was still recorded as the active conversation, so later edits autosaved against a foreign key and a reload mounted straight onto the destination, losing the work that had just been preserved. * fix: address eighth round of PR review bot findings Make submitted-use evidence durable and readable across tabs. The tab that retries a retained deletion is rarely the tab that sent the message, and this evidence lived in the sending tab's session storage, which left published tab presence as the only cross-tab record. That ages out on a fixed ten-minute window, so a retry resuming after a longer freeze classified a sent file as abandoned and deleted it out of its message. It is timestamped in localStorage now, with a horizon wide enough to outlast any plausible freeze and a hard cap so a long-lived profile cannot grow it without bound. Paste provenance stays in session storage, since which chips offer the paste affordances really is per-tab. Tie a blocked pending draft to its intended destination. Keeping the pending key active while a live tab owned the destination left the pending state with no memory of where it was heading, so any later navigation looked like the awaited transition and carried the queued text and attachments into an unrelated conversation. Defer an in-flight paste on direct conversation resets. Callers that reach newConversation without going through New Chat left an upload with no filepath yet unrecorded, so once the request landed nothing remained to delete the server file. * fix: restore the composer clear after send CI e2e caught this: after sending a message with an attachment the chip stayed in the composer, so the sent message and the composer both showed it. Two causes, both from keying composer storage off state that lags a render. `currentConversationId ?? conversationId` is the previous conversation during every transition, so the file-cache restore ran against the outgoing key and put the just-sent attachment straight back into the map that the submit had cleared. The active key is now the conversation unless the switch effect has deliberately parked storage on the pending key. Separately, treating any first mount as the awaited pending transition ran the pending migration on every direct load of a conversation. That is narrowed to a pending record this tab owns which actually holds something, which is what a reload with real queued work looks like. Verified against the two failing specs locally, then the whole mock chat spec: 8 passed. * fix: address ninth round of PR review bot findings Stop expiring submitted-use evidence on a timer. The work it has to outlast is a retained deletion, and those carry no expiry of their own, so any interval chosen could be outlived by a suspended tab still holding cleanup work, which is the same bug with a longer fuse. The ledger is bounded by count instead, evicting oldest first only when it would otherwise grow without limit. Consult that ledger before retrying a deletion. The retry pass built its protection set from drafts and published presence only, both per-tab and time-bounded, so a file sent from a tab that has since been suspended had nothing left to speak for it. The record is resolved before judging, because the discard is often keyed by the temporary upload id while the pane that sent it marked only the server id. Refresh liveness when withdrawing presence, matching the publication side. A retained-deletion pass resuming after paused timers withdrew its own entry and then swept the record as stale, taking sibling panes' claims with it. Keep queued attachments when neither draft key is writable. With another tab owning the pending key and a second owning the destination, the effect cleared the live map and could persist it nowhere, so unsent attachments vanished the moment the run got its conversation id. Remove the replacement provenance when an edited paste is not accepted. The edit path records the replacement id before routing the upload, and a rejected upload left a provenance-only draft that reads as a live attachment claim with no chip behind it. Verified with the mock chat e2e spec after rebuilding the frontend: 8 passed. |
||
|
|
6284139f18 |
📦 chore: bump @librechat/agents to v3.7.2
|
||
|
|
1489623fa3
|
🎬 test: Record-Once/Replay-Forever Model Fixtures for Mock E2E (#15210)
* 🎬 test: Record-Once/Replay-Forever Model Fixtures for Mock E2E The mock e2e lane's only credential-free model is a hand-authored script: `fake-model.js` decides responses from ~60 `E2E_*` prompt markers. That covers scripted shapes well, but no scenario replays a *real* recorded provider conversation through the assembled chain, so real streaming shapes — provider chunk cadence, reasoning deltas, usage metadata — are only ever approximated. This adds a record-once/replay-forever tier alongside the marker routing. Record (`E2E_MODEL_FIXTURES=record`, needs a provider key): the run hook appends a LangChain callback handler to every agent context's `clientOptions.callbacks` instead of overriding the model, so the REAL provider streams while each invocation's `AIMessageChunk`s serialize to `e2e/fixtures/model-replay/<name>.jsonl` — text deltas, tool_call_chunks, reasoning kwargs, and genuine usage metadata. Only the latest human text is recorded for binding; system prompts and tool schemas never enter the fixture. Replay (default, keyless): `fake-model.js` consults `tryBindReplay` ahead of marker routing, binding a conversation whose prompt matches the next unconsumed invocation. The replaying model is not hand-assigned — it is registered as SDK provider `librechat-e2e-replay` via `registerProvider` and constructed through the SDK's own `initializeModel`, so registry lookup, constructor clientOptions, and real `bindTools` all run the way a live provider's would. Recorded chunks therefore stream through the same createRun → graph → SSE → persistence chain. Consumption is enforced rather than assumed: every invocation re-checks its prompt against the recording, an invocation past the end of the script throws, and a per-fixture ledger lets the spec assert at teardown that every recorded invocation and chunk was drained. Streaming incrementality is asserted from that ledger, not by sampling transient DOM, which is a race by construction. The credential-free profile is unchanged when not recording: the record provider and its selector entry are template markers that stay comments, and no existing spec's routing is touched (a fixture only binds on an exact prompt match; everything else falls through). Verified: record vs the real DeepSeek API 1 passed (15.2s); keyless replay 1 passed twice (11.4s, 12.7s) with the ledger fully drained (2/2 invocations, 10/10 chunks, no overruns or mismatches); app-load, completion, and chat 10 passed unchanged. * fix(e2e): rebind a replay fixture from the top for a new conversation The replay cursor is process-global while the web server outlives a Playwright retry, so a fully consumed fixture left the retry unable to bind its first prompt: it fell through to marker routing and failed deterministically, burning every configured CI retry. A partially consumed attempt failed the same way. Binding now restarts the fixture when the incoming prompt matches its first recorded invocation, resetting the ledger with the cursor so the new attempt is judged on its own consumption instead of accumulating the previous one's counts. Continuing an in-progress binding still outranks restarting, so a fixture whose opening prompt repeats later in the script advances rather than rewinding. The over-consumption guard is untouched — it fires inside the stream when the cursor passes the end, not at bind time. * fix(e2e): close three replay-lane gaps found in review Restart the recorder on a retry. Its state is process-global like the replay cursor, so a failed attempt that had already recorded invocations left the counter advanced: the retry appended 2/3 after 0/1, or kept the previous attempt's `error` line, and the fixture was unusable for replay. Recording now truncates and restarts when the opening prompt reappears, mirroring the replay side's rule and its caveat. Retain a consumed binding for the conversation that drove it. An extra user turn past the final recorded invocation found no next invocation and fell through to ordinary fake-model routing, so it was answered with a mock reply: the over-consumption guard never ran and the already-drained ledger still passed. Such a conversation is now recognized by its human turns opening with the fixture's recorded prompts, and stays bound so the stream raises the overrun. Continuing an in-progress binding still outranks restarting, which outranks retaining a consumed one, so a retry's fresh conversation rewinds rather than being read as an extra turn. Validate the fixture the recording actually wrote. Record mode honors `E2E_MODEL_FIXTURE_NAME`, but the spec always inspected the committed `deepseek-two-turn`; another name wrote elsewhere while the assertions read the pre-existing file, and because the prompts are fixed the stale answers could match and green a run that verified nothing it produced. * fix(e2e): make replay binding correct for tool and subagent fixtures Round two's retry and consumed-binding fixes both assumed one model invocation per user turn. A turn that calls a tool breaks that: the model is invoked again after the tool result under the same latest human message. Identify a retry by the conversation boundary, not the prompt. The recorder ran per invocation and truncated whenever the opening prompt reappeared, so a tool round trip looked like a retry and discarded the recorded tool-call invocation. Restart detection now sits in `installRecorder`, which runs once per `createRun`: a turn whose history holds no prior human message begins a conversation. Compare consumed bindings against user turns, not invocations. Several recorded invocations can share one prompt, so a one-to-one comparison could not recognize the originating conversation — invocations `[A, A, B]` against history `[A, B, C]` failed on both length and elements, and the extra turn fell through to the fake model with the drained ledger still passing. Fixtures now carry their collapsed turn sequence. Override the subagent model too. `graph.overrideModel` is not inherited by child executors, so a fixture recording a subagent call — record mode captures child invocations already — would leave the child on its configured provider: an underrun, and a real provider request in a lane that must stay keyless. Reject ambiguous prompt matches. Binding order followed filesystem enumeration, so a second fixture sharing a prompt could silently redirect a scenario to the wrong chunks and ledger; the spec's choice never reaches the server-side loop, so ambiguity fails instead of picking a winner. Fixture identity is the file name for the same reason — a recorded `meta.name` is descriptive, and trusting it let a copied fixture collapse onto another's registry key and ledger. Prove the recording is fresh. The spec removes the selected fixture before driving, so a run whose hook never installed the recorder fails instead of greening against a stale artifact whose answers still match these deterministic prompts. * fix(e2e): rewind a replay fixture at the conversation boundary Consecutive invocations can share a prompt — a tool call produces exactly that — so an attempt stopping mid-turn left the cursor on an invocation whose text still equalled the opening prompt. Matching the cursor first meant a retry's fresh conversation resumed after the tool call instead of rewinding, consuming the post-tool invocation and silently replaying a different script than was recorded. A conversation boundary now outranks an in-progress cursor: a fresh conversation whose prompt opens the fixture rewinds even when the cursor would have matched. Continuing still outranks restarting within a conversation, so a turn that calls a tool advances to its post-tool invocation rather than rewinding on its own repeated prompt. * fix(e2e): refuse cross-conversation binding and prove content streaming A fresh conversation could steal a partly consumed fixture's later turn. Only a conversation opening with the fixture's first prompt was treated as a boundary, so after `[A, B]` had consumed `A`, an unrelated new conversation whose first message was `B` matched the cursor, received the recorded second-turn response, and advanced the shared cursor without ever having driven `A`. A conversation start may now only rewind a partly consumed fixture, never continue it; continuation within a conversation is unaffected. The incrementality assertion counted empty frames. Providers emit empty initialization and usage-metadata chunks around the content deltas, so a total chunk count above one was satisfied by a single delta: the previous fixture's closing turn had four chunks and one content-bearing delta carrying the whole answer, and both modes stayed green without proving incremental assistant-content streaming at all. Fixtures now track content-bearing chunks separately, the closing prompt asks for prose rather than a number, and both modes require several content deltas on that turn. Re-recorded: the closing turn now carries 28 content deltas. * fix(e2e): scope record mode to the fixture spec `E2E_MODEL_FIXTURES=record` replaces the fake-model hook globally, so an unfiltered entry point such as `npm run e2e:mock` sent every spec under specs/mock to the paid real-provider endpoint, while each fresh conversation truncated and rewrote the one selected fixture — leaving an artifact from whichever scenario happened to run last. Record mode now matches only the fixture spec: an unfiltered recording run lists one test instead of 203. Replay mode is untouched and still collects the full suite. * 🪪 fix: Bind Replay Fixtures by Conversation, Not Prompt Text Prompt text was standing in for conversation identity, and three review rounds found the same class of defect underneath it: a tool call repeats a prompt across invocations, a retry repeats it across attempts, and a resumed run has neither prompt nor history because `createRun` is rebuilt with no messages while state comes from the checkpoint. Each fix in that space created the next gap. Thread the identity instead. `createRun` accepts a `conversationId` and passes it to the run hook, which the agents controller supplies at both call sites — the same value it already uses as the checkpointer's `thread_id`. The field is optional and the hook is env-gated, so nothing changes when the harness is not in use. Binding then collapses to ownership. A fixture is owned by the conversation that claimed it, and its cursor is authoritative wherever it stands: an extra turn reaches the over-consumption guard rather than falling through to the scripted fake model, and a resumed run keeps replaying with no prompt to match. A different conversation may claim the fixture only by opening it, which rewinds — what a Playwright retry looks like. Everything else is refused, so an unrelated conversation can no longer continue someone else's partly consumed script by repeating a later prompt. The prompt is still re-checked on every real turn; only a resume, which structurally carries no human message, is exempt. The previous text-and-history rules remain as a fallback when identity is absent. The recorder keys the same way: a new attempt is a new conversation, so a resume no longer truncates the fixture mid-turn and discards its tool-call invocation. Record summarization too. The summary provider runs on its own model with its own callback list, so a scenario crossing the context-pruning threshold recorded the agent's invocations but not the summariser's, leaving a fixture that could not reproduce the pruned context. * 🧾 fix: Harden Record Mode and Make the Rendered-Text Assertion Honest CI caught what local runs had not: the committed fixture was never replayed locally, because the record run overwrote it after the replay check rather than before. Re-recording and replaying in that order is what surfaced the rest of this. The DOM assertion compared raw recorded text against rendered markdown. The previous answer opened with `52.`, which Markdown renders as an ordered-list marker, so those characters never appear in the DOM and the match failed on all three CI attempts while replay itself was correct. The closing prompt now asks for prose beginning with a word, a leading enumerator is stripped before matching, and only a prose prefix is compared. Derived configs discarded the record-mode restriction. `config.redis.ts` and `config.mermaid.ts` spread this config and then replace `testMatch`, so `e2e:mock:redis` in record mode would still send its specs to the paid provider. A restriction expressed as an overridable value cannot hold, so record mode now refuses any config but the mock one. Superseded recording callbacks could write across a reset. A failed attempt with a provider call still in flight keeps its handler on the old graph; after the retry reset, that call would allocate an invocation from the new counter or append an `error` entry with a cleared mapping. Handlers now carry the recording generation they were installed for and ignore everything from an older one, and attachment dedupes against the current generation so a graph carried across a restart is not left with an inert handler. * 🚧 fix: Make Summarization an Explicit Boundary, Not a Half-Feature Recording summarization invocations without replaying them is worse than ignoring them. Replay routes the agent model and subagents only, so a recorded summarization entry takes a slot in the fixture sequence that replay never consumes, and the next primary call reads the summariser's chunks — a prompt mismatch or, worse, silently wrong content. The attachment was also aimed at the wrong shape: the SDK reads `summarizationConfig.parameters`, not `.parameters` nested under `.config`, so the previous attempt would have attached to nothing in a real run. Its test passed only because the test built the shape the code expected rather than the shape the SDK provides. Rather than ship a half-routed feature, recording now fails the moment summarization runs, naming the reason. Both shapes are guarded so the guard cannot miss the way the recorder did. Summarization fixtures need replay routing for that model before they can be supported. The derived-config guard added alongside it was itself broken: workers do not carry `--config`, and the argument lookup fell through to `process.argv[0]`, so every recording run aborted claiming the node binary was an unexpected config. The flag is now located explicitly and absence is treated as "not the process that parsed the CLI". * 🔒 fix: Close the -c Config Alias and Pin the Recorder's Fixture Name Playwright documents `-c` as an alias for `--config`, so record mode launched as `playwright test -c e2e/playwright.config.redis.ts` slipped past a guard that recognised only the long spelling. Both spellings and both `=` and space forms are now parsed. Accepting arbitrary fixture names also worked against the ambiguity check. This spec drives one fixed prompt pair, so recording under another name left two fixtures sharing those prompts; replay then refused to bind either and the keyless lane stopped working — a successful documented recording run could disable the suite it exists to serve. The spec now records only the fixture it owns and says so when asked for another. * 🔧 test: Record a Real Tool-Call Turn and Replay It Through the Tool Node The fixture format carried `tool_call_chunks` and the binding advanced through a turn's invocations, but nothing had recorded a real tool-calling conversation end to end — the path was covered only by hand-written synthetic fixtures, and it is the first one a new scenario would exercise. This records one: the provider calls the `remember_fact` MCP tool, the tool runs, and the model is invoked a second time with its result. That is the shape a single prompt cannot express — one user turn spanning several model invocations, all sharing one prompt — so it is what proves the turn-vs-invocation distinction the binding rules were built around. Replay drives the real tool node rather than replaying its output, so the tool executes again and the assertion checks its live result. Two fixtures now coexist, which the record path had to grow for: the config keeps an allowlist so an unknown name is still refused, record mode collects every replay spec, and each spec records only the fixture it owns and stands down for the others. MCP tools reach the model under a server-qualified name (`remember_fact_mcp_e2e-memory`); that qualification has changed before, so the assertions match the base name as a prefix rather than pinning the suffix. Verified: record 1 passed (15.6s, real API) then replay 1 passed (13.6s) against that fixture, ledger drained 2/2 invocations and 35/35 chunks; both replay specs together 2 passed; app-load, completion, chat and mcp-ephemeral 12 passed. |
||
|
|
290b8664d9
|
🧾 feat: Track Authoritative Agent Event Outcomes (#15213)
* feat: track authoritative agent event outcomes * fix: isolate agent event outcome types * fix: declare agent event handler result * fix: simplify agent event status selection * fix: preserve authoritative event outcomes * fix: preserve terminal event evidence * test: use completed run-step envelope * test: scope deferred HITL question locator * fix: settle every agent event terminal path * style: sort terminal host action imports * fix: fence agent event terminal evidence * fix: recover agent event terminal settlement * fix: scope terminal retry hints by generation * fix: settle terminal host actions exactly |
||
|
|
6d499ba3ce
|
⏳ fix: Anchor Resumed Elapsed Time at the Generation's Real Start (#15204)
Some checks are pending
Publish `@librechat/client` to NPM / pack (push) Waiting to run
Publish `@librechat/client` to NPM / publish-npm (push) Blocked by required conditions
Publish `librechat-data-provider` to NPM / pack (push) Waiting to run
Publish `librechat-data-provider` to NPM / publish-npm (push) Blocked by required conditions
Publish `@librechat/data-schemas` to NPM / pack (push) Waiting to run
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* ⏳ fix: Anchor Resumed Elapsed Time at the Generation's Real Start A reload emptied the Recoil anchor, so the indicator fell back to its mount time and visibly reset to 0s over a run that had been generating for much longer. The stream status the resume path already reads carries the server-recorded generation start; the fill now prefers it, so a reattached run reports real elapsed time. The fill remains fill-only: a same-session reattach keeps its original ask baseline, and the indicator's existing clamp absorbs any client/server clock skew. * 🕰️ fix: Rebuild the Resumed Baseline From the Server-Computed Age Codex round 1: anchoring at the server's raw createdAt compares two clocks — a client behind the server froze the resumed reading at 0s for the skew, one ahead inflated it. The status route now also reports the generation's age computed on its own clock, and the client rebuilds a clock-local anchor as Date.now() minus that age, so each machine only ever compares to itself. Raw createdAt stays as the fallback for an older server mid-rolling-deploy. * 📥 fix: Compute Elapsed Age in TypeScript, Anchor It at Status Receipt Codex round 2: the elapsed computation moves into packages/api as getGenerationElapsedMs — the route now just delegates, keeping the response contract type-checked and the /api surface a thin wrapper — and the client subtracts the age from the moment the status response arrived (dataUpdatedAt) rather than from apply time, so a slow history fetch between receipt and apply can no longer shrink the reading. Declined with rationale: a shared clock source across replicas — the residual is inter-replica NTP drift, milliseconds against the minutes-scale client skew this PR eliminates, and the helper gives any future shared-clock upgrade a single home. |
||
|
|
d9e6250d05
|
🛑 fix: Separate Agent Event Backpressure From User Bans (#15200)
* 🛑 fix: Separate Agent Event Backpressure From User Bans
* fix: Address Agent Event Review Findings
* fix: Mirror Case-Insensitive Agent Control Routing
|
||
|
|
877b9b2f1a
|
🐚 feat: Nonce-Based Content Security Policy for the SPA Shell (#14446)
* 🛡️ feat: Configurable Baseline HTTP Security Headers Adds helmet's CSP-independent headers (HSTS, X-Frame-Options, X-Content-Type-Options, COOP, CORP, Referrer-Policy) on every response, with contentSecurityPolicy explicitly disabled. Every header that can break a deployment is configurable, so there is no allow-list to go stale the way #7377's hardcoded CSP directives did. HSTS includeSubDomains defaults off rather than matching helmet's on-by-default: it would otherwise pin every sibling subdomain to HTTPS for a year in every visitor's browser, and undoing that requires serving max-age=0 from each affected host. * 🛡️ feat: Nonce-Based Content Security Policy for the SPA Shell Adds an opt-in, per-response nonce CSP on the HTML response, resolved once at startup so each request only mints a nonce and concatenates the header. Report-only by default, since that is the rollout step #7377 skipped. Rebase and correctness pass over #13226: - Styles carry no nonce. A nonce in style-src makes browsers ignore 'unsafe-inline', which would have blocked the <style> element the theme script injects at runtime, plus every style third-party components inject. - frame-ancestors 'self' is now a default rather than opt-in, so enabling CSP actually covers the clickjacking half of #7110. - CSP_SCRIPT_SRC_EXTRA now drops 'strict-dynamic', which would otherwise make browsers ignore the very hosts the operator configured. - Nonce stamping runs after the query-devtools bootstrap injection so that injected script is covered too. * fix: replace frame-ancestors instead of merging it Merging the configured value into the default turned a deliberate CSP_FRAME_ANCESTORS='none' into `frame-ancestors 'self' 'none'`, which browsers resolve back to 'self'. Also bail out if the serialized policy somehow lacks the nonce slot rather than emitting a header the shell cannot match. * fix: address Codex review findings on the CSP defaults All five were real against LibreChat's actual runtime: - CSP_REPORT_ONLY now only enforces on an explicit false/off/0/no. A typo or `1` previously fell through isEnabled() to enforcing, turning a config slip into a blocked SPA. Shares the parse helper with headers.ts via a new security/env.ts. - Module preloads are stamped. A production client/dist/index.html carries 32 parser-inserted `<link rel="modulepreload">` tags, which 'strict-dynamic' does not cover and 'self' cannot rescue. - Stale nonce attributes are replaced rather than preserved; only the current response's nonce is authorized. - worker-src allows data:, which Monaco's default CDN loader needs to bootstrap its workers (there is no loader.config() in the client). - script-src allows 'wasm-unsafe-eval' for the HEIC upload path, which compiles WebAssembly through heic-to. Narrower than 'unsafe-eval'. Verified against the real built shell: 4 scripts and all 32 preloads nonced, stylesheets/icons/manifest and <style> untouched. * fix: address second Codex round on CSP rollout controls - SECURITY_HEADERS=false now disables CSP too. It is documented as the global kill switch, and an operator reaching for it to recover a shell broken by an enforcing policy must not be left with that policy on. - The SPA shell is forced to `no-store` while CSP is enabled, ignoring INDEX_CACHE_CONTROL/INDEX_PRAGMA/INDEX_EXPIRES and warning when they are set. A cacheable shell pins one nonce across page loads and users, which is the whole thing a nonce policy defends against. - Added CSP_ALLOW_WASM and CSP_ALLOW_DATA_WORKERS. The previous commit's .env.example claimed CSP_ADDITIONAL_DIRECTIVES could drop 'wasm-unsafe-eval' and data:, but merging only ever appends sources, so the documented hardening step was impossible. These toggles make it real. |
||
|
|
018775de07
|
🧾 fix: Honor Disabled Transactions on the Assistants Usage Path (#15100)
* 🧾 fix: Honor Disabled Transactions on the Assistants Usage Path Thread the resolved transactions config through `recordUsage` from each of its callers, so `transactions.enabled: false` is honored on the assistants token spend path. * 🧾 fix: Thread the transactions config through the vision-request caller Address review: `ToolService.processVisionRequest` also records usage without the resolved config, and `recordUsage`'s documented return type did not match the function. * 🧾 fix: Set the resolved transactions config after the usage spread - provider usage could carry a `transactions` key that overwrote the trusted value - matches the ordering the other `recordUsage` callers already use |
||
|
|
2ef12b1e1d
|
🦺 feat: Configurable Baseline HTTP Security Headers (#14445)
Adds helmet's CSP-independent headers (HSTS, X-Frame-Options, X-Content-Type-Options, COOP, CORP, Referrer-Policy) on every response, with contentSecurityPolicy explicitly disabled. Every header that can break a deployment is configurable, so there is no allow-list to go stale the way #7377's hardcoded CSP directives did. HSTS includeSubDomains defaults off rather than matching helmet's on-by-default: it would otherwise pin every sibling subdomain to HTTPS for a year in every visitor's browser, and undoing that requires serving max-age=0 from each affected host. |
||
|
|
4b113697b5
|
🔌 feat: Background Execution Toggles for Actions & Plugin Tools (#14407)
* 🧵 feat: Background Execution Toggles for Actions & Plugin Tools * 🩹 fix: Resolve action background opt-in across encoded-domain forms and scope it per action * 🧹 refactor: Resolve action domain in a single pass * 🧩 fix: Merge Normalized Action Background Options * 🪢 fix: Reconcile Action Background Aliases * 🧭 fix: Harden Action Background Compatibility * 🕰️ test: Allow Settled Task TTL Expiry * 🧬 fix: Merge Refreshed Action Tool Registrations |
||
|
|
4d246469dd
|
🧾 fix: Honor Disabled Transactions on the Abort Paths (#15099)
Resolve the transactions config from the request and forward it to both abort write paths, so `transactions.enabled: false` is honored when a generation is stopped. |
||
|
|
862ebf3235
|
🪢 fix: Persist Failed Agent Turns Before Error Publication (#14118) | ||
|
|
ac2aef00f6
|
🫗 fix: Drain Quoted Excerpts Into Mid-Run Steering (#15175)
* 🧭 fix: Carry Quoted Excerpts Through Mid-Run Steering "Add to chat" quote chips were dropped by every during-run steer path: the steer POST had no quotes concept, so a composer-origin steer left the chip staged (gluing onto the NEXT send) and a queued item steered into the live run lost its quotes silently. Quotes now ride the steer protocol end to end: - POST + admission: `quotes` on the steer body, normalized like the chat route's (getReferencedQuotes caps), part of the idempotency fingerprint only when present so pre-existing receipts still replay. - Injection: merged into the model-bound turn as Markdown blockquotes at both boundaries (text-only and media paths), mirroring prependQuotes. - Persistence + replay: the STEER content part stores `quotes` separately from the typed text; stampSteerPartMedia re-merges them per turn (even with resendFiles off) via the SDK's transient media stamp, with the quote block folded into the token budget. - UI: composer steers/interrupt-steers drain the chips (skill picks stay staged — they configure a NEW turn's run); SteerPart and the in-flight bubble render the same MessageQuotes reference blocks as user bubbles; queued/failed rows show a quote count; reconnect reseeds fall back to the server item's quotes when no local chip survives. - buildMessages keeps its zero-await path to the parallel context kickoff via a synchronous stamp-target probe. * 🧭 fix: Keep Quotes in the Client-Safe Steer Projection toPendingSteer is the projection behind resume-state pendingSteers, abort responses, and terminal leftover claims — dropping quotes there would lose them on exactly the recovery paths the reconnect reseed's server fallback relies on. * 🧪 test: In-Flight Steer Bubble Renders Carried Quotes * 🔁 fix: Re-Stage Quotes When a Pre-Quotes Replica Accepts the Steer Codex flagged the rolling-deploy window: an old replica 202s a quoted steer while dropping the excerpts, so the client cleared the chips for context the model never received. The 202 (fresh and receipt replay) now echoes quotesAccepted from the DURABLE item; a missing echo on a quote-bearing composer-origin steer re-stages the excerpts as composer chips — the pre-steer behavior, so they ride the next send instead of vanishing — and strips them from the surviving chip so a later terminal conversion cannot duplicate them. Queued-origin steers keep quotes on the item, whose restore paths already return it intact. The residual cross-version lost-ACK retry stays fail-closed as a 409 idempotency conflict (failed chip with retry controls). * 🔁 fix: Close the Remaining Cross-Version Quote-Loss Windows Codex round 2: - Send now of a quoted queued item against a pre-quotes replica now re-stages the excerpts too (the row is consumed and the words inject bare, so the composer is their only remaining home); the strip clears the chip's captured origin copy so reclaims and terminal conversions cannot duplicate them. - A quoted retry whose lost first ACK was accepted by a pre-quotes replica now REPLAYS that legacy receipt instead of 409ing: the stored fingerprint matching the quote-less hash of the same words proves the cross-version case, and the replayed 202's missing echo drives the re-stage. Different quotes against a quote-bearing receipt still conflict. - TSteerAppliedEvent.part gains the quotes field (typed SSE consumers). * 🧪 test: Drop the Stale Narrow SteerDrainOutput Alias The spec's local intersection re-declared injectedMessages with content: string, predating the SDK pin that declares the field natively (content: string | MessageContentComplex[]). Under CI's clean install the hook's BaseHookOutput is no longer assignable to that narrower alias; the plain PostToolBatchHookOutput is the correct type for every drain/boundary assertion. Verified against the published 3.6.16 dist and the local one. * 🔁 fix: Honor the Generation Owner's Quote Capability End to End Codex round 4: - steerQuotesCapable rides job metadata (createJob + HITL resume rewrite), mirroring preemptCapable's owner-recorded pattern: an upgraded admission replica no longer stores quotes — or claims them accepted — for a generation whose older owning drain would silently drop them at injection. The missing echo drives the client re-stage, and a later capable handover cannot double-deliver restored context. - Applied events reconcile dropped quotes: when a quote-less applied part settles a quote-bearing chip (the lost-202 ordering the ACK-echo path cannot see), resolveSteerChip and both reconnect settle paths re-stage the chip's excerpts before removing their only copy. mergeRestagedQuotes dedupe keeps every trigger idempotent for the same excerpts. * 🔁 fix: Re-Read Quote Capability at the Last Moment and Cap Restaged Chips Codex round 5: - A HITL resume rewrites steerQuotesCapable without changing the generation's createdAt, so the enqueue fence cannot see a capable-to-legacy handover landing during admission's awaits. Re-read the owner's flag immediately before item construction (paid for only by quote-bearing requests); the residual between re-read and enqueue commit matches preemptCapable's documented race. - mergeRestagedQuotes now respects the 10-quote contract with the staged chips winning: a restored tail that cannot ride the next send is dropped explicitly instead of rendering as a chip the submission would silently discard. MAX_QUOTE_COUNT moves to utils/steer as the single client source; QuoteButton imports it. * 🔁 fix: Steer Quote Coverage for Preflights, Memory, and Single-Scan Stamping Codex round 6: - Stored-message policy inspection now extracts steer-part quotes as quote fragments (path /content/N/quotes/M), so conversation import and shared link preflights inspect the newly persisted field exactly like top-level message.quotes. - The memory copy gets its own quote-merge stamp (text only, resendFiles false): formatAgentMessages ignores part.quotes, so without it a steer whose substance lives in its excerpt reached the chat model but never memory extraction. - collectSteerStampTargets replaces the boolean probe: buildMessages collects once and hands the targets to stampSteerPartMedia, keeping the zero-await fast path without scanning the history twice. * 🔁 fix: Redis Quote Plumbing, Conversion-Race Guard, and Quote-Bound Recovery Proof Codex round 7: - RedisJobStore.deserializeJob now restores steerQuotesCapable (the explicit mapper otherwise dropped it on every read, leaving quote steering inert in Redis deployments), with the round-trip spec extended. - Both Lua parked-steer projections (terminal close + generation replacement) forward item.quotes, matching toPendingSteer — a lost final no longer strips excerpts from durable recovery in Redis mode. - The no-echo restage reads the SURVIVING chip (reclaimRejectedChipQuotes): a terminal conversion that beat the delayed 202 already moved the quotes onto the queued follow-up, and re-staging them again double-delivered. Regression-tested with the conversion-before-ACK ordering. - RecoveredSteerPayload binds normalized, order-significant quotes (builder, validator, TS matcher, and the Lua decode+matcher): a stale client presenting the same recoverySteerId with altered or missing quotes cannot consume the parked source. Quote-less sources keep matching quote-less recoveries. * 🔁 fix: Execution-Bound Quote Capability with an Atomic Enqueue Predicate Codex round 8: - steerQuotesCapable becomes a transient assertion translated (at createJob and in ApprovalLifecycle.resolve) into steerQuotesExecutionId, valid only while it equals the LIVE providerExecutionId. A legacy replica winning a HITL resume rewrites the execution id without knowing the marker, so its stale assertion self-invalidates — a bare boolean could not be cleared by code that predates it. - The fenced enqueue evaluates that equality atomically (all three Redis scripts decode-and-strip like the existing preemptCapable normalization; both InMemory sites mirror it) and returns the persisted item, so the quotesAccepted echo reflects exactly what was stored even when a handover lands between admission's read and the commit. The last-moment re-read is gone — the transaction is the authority. - Tests: capable-resume re-binding, legacy-resume omit-not-clear invalidation, the admission-vs-handover race (capability read true, then execution rewritten before enqueue), and the Redis round-trip of the marker. * 🔁 fix: Full Redis Parking Coverage and Loss-Moment Quote Restaging Codex round 9: - The two remaining Redis parking projections (terminal status CAS and stale-running cleanup) forward item.quotes — every field-picked steer projection now carries them (audited: 2 Lua 'projected' + 2 Lua 'clientItem' + toPendingSteer). - The ordinary no-echo ACK no longer re-stages: the steer has not injected yet, so the quotes stay carried on the pending chip. A quote-less applied event re-stages them at the actual loss; a terminal leftover conversion carries them onto the recovered row, whose normal send delivers quotes on any server — re-staging at the ACK let that leftover auto-send bare text while the excerpts glued onto an unrelated draft. Only the settled receipt replay (already injected, no future event) reclaims immediately. * 🔁 fix: Legacy-Replayable Receipts with Separate Quote Identity Codex round 10: an upgraded-first receipt stored a quote-inclusive fingerprint no pre-quotes replica could recompute, so a lost-ACK retry routed through one 409'd already-accepted words with duplicate-send controls. The durable fingerprint reverts to the quote-independent 3-field hash — the one shape EVERY deployed version computes, replayable across a rolling deploy in both directions — and quote identity moves beside it as requestedQuotesFingerprint (of the REQUESTED quotes, pre any capability strip, so an incapable-owner acceptance still replays its own retries). Absent records (legacy-written or quote-less) accept any same-words retry, preserving the round-5 rule; present records must match exactly, keeping different-quotes clientSteerId reuse a 409 on quote-aware readers. Under the keep-on-chip client contract a legacy replay's missing echo is harmless — the excerpts stay carried on the pending chip. * 🧪 chore: Re-Trigger CI After Dropped Workflow Events |
||
|
|
69e7c73614
|
🎛️ feat: Expose Authoritative Subagent Controls (#15169)
* feat: expose authoritative subagent controls * fix: reconcile subagent control races * fix: reconcile durable control conflicts * fix: preserve authoritative subagent control outcomes * fix: fence subagent controls to child thread * fix: validate subagent control targets before routing * fix: close subagent control boundary gaps * fix: keep control reservations private * fix: close subagent control admission gaps * fix: preserve authoritative control history * style: sort subagent control imports * fix: preserve authoritative subagent control retries * style: sort control state imports |
||
|
|
d641c398d5
|
🧳 fix: Port Subagent Control Receipt Writes to DocumentDB-Safe Operators (#15171)
* fix: harden subagent control receipt persistence * fix: harden durable subagent control replay * fix: await terminal control receipts on shutdown * fix: close subagent control replay races * test: type stale-owner transport fixture * fix: quiesce durable subagent controls * test: await subagent shutdown durability boundary * fix: serialize durable subagent controls * fix: fail shutdown on cleanup errors * fix: report cancellable result availability accurately * fix: fence subagent control receipt ownership * fix: close distributed control receipt races * test: type control receipt race fixture * chore: require authoritative control receipts * fix: close subagent control lifecycle races * style: separate control reservation member * test: harden subagent settlement wait * fix: preserve authoritative control replay state |
||
|
|
afcf2e886c
|
📦 chore: bump @librechat/agents@latest to v3.7.1 (#15176)
|
||
|
|
a997275902
|
🧾 feat: Persist Authoritative Subagent Control Receipts (#15168)
* feat: persist subagent control receipts * fix: require control receipt persistence * fix: preserve authoritative control history |
||
|
|
5a8700643c
|
⚡ perf: Build the Memory Message Copy Only When Something Reads It (#15164)
buildMessages formatted every history row twice per turn — a prompt copy and a memory copy — then discarded the entire memory payload unless some row carried fileContext, which is the rare case. The memory copy has exactly two consumers: that payload, and the canonical recount of a row, where it is content-identical to the prompt copy unless the row itself has fileContext. So the prompt copy is now the recount surface for context-free rows, a fileContext row builds its memory copy at recount time, and the full memory payload is assembled in a deferred pass — same formatting, same per-row merge order — only once a row has proven the payload will be kept. The common turn formats each row once instead of twice and no longer allocates a payload it throws away. Also forwards the run's useLegacyContent to formatAgentMessages as the new legacyContent option, inert on the current SDK release: once the SDK change ships, text history is emitted pre-flattened so the per-request legacy projection stops cloning every message and the context meter's identity-based count reuse holds across the projection. |
||
|
|
bf1e13b806
|
🥁 fix: Compare TOTP Codes in Constant Time (#15157)
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com> |
||
|
|
18cc47128d
|
chore: bump agents sdk to v3.7.0 (#15163) | ||
|
|
9cee6f97cc
|
🧩 chore: Bump Agents SDK to v3.6.16 (#15151) | ||
|
|
d85bea41cc
|
🧪 ci: Mock the Parent Subagent Index Handler in Convos Route Tests (#15146) | ||
|
|
fc2b8584c4
|
📇 feat: Surface Event Child Activity Through a Bounded Parent Index (#15142)
* feat: surface event-driven child activity * fix: keep child task aggregation documentdb-compatible * fix: address event activity review findings * test: provide markdown message context defaults * fix: report bounded child history truncation * fix: preserve current child activity state * fix: preserve durable event child activity * fix: handle missing task timestamps * fix: keep active event snapshots live * fix: preserve event activity across valid anchors * fix: close event child activity gaps * fix: preserve event activity across resume |
||
|
|
d864597731
|
⚡ perf: Append Saved Message Ids Instead of Rebuilding the Conversation Array (#15141)
* ⚡ perf: Append Saved Message Ids Instead of Rebuilding the Conversation Array Every saveConvo read every message id in the conversation (sorted) and wrote the array back onto the document — twice per chat turn, O(n) in conversation length, from a write path. The turn's savers know exactly which message they just wrote, so they now pass it as metadata.appendMessageIds and saveConvo $addToSet-s it, skipping the read and the full-array rewrite. Every save without the option — titles, archive, fork, import, threads — still rebuilds from the database, which remains the heal point for the drift that message deletion has always left behind (deletes never ran saveConvo). The array's consumers read presence or length, or use it as an optimistic cache placeholder, so incremental maintenance is behaviorally identical; on traced turns the array stays exactly equal to the messages collection. Per-turn queries: 15 -> 13 (two Message.find gone), and the growing array payload no longer crosses the wire twice per turn. * 🎯 fix: Brand the Lineage-Only Resolved Conversation Instead of Guessing by Shape The resolved-conversation files fast path treated an absent files property as unresolved so the lineage-only partial from a bound agent-event continuation could not silently hide a conversation's uploads. But MongoDB never stores an empty files array, so nearly every real conversation also lacks the property and the fast path never fired — a follow-up turn on an upload-free conversation still paid the getConvoFiles round trip. The synthesized partial is the one object that cannot speak for the database, so it now carries an explicit symbol brand (PARTIAL_RESOLVED_CONVERSATION, non-serializing and invisible to key iteration), and a stored document without files means what it means: no files. Traced follow-up turns drop from 14 queries to 13. * 🧪 test: Expect the Appended Message Id in the Route's saveConvo Metadata messages-get.spec.js pins the exact metadata POST /api/messages passes to saveConvo; the route now forwards the saved message's _id as appendMessageIds, which is the behavior the append path depends on. |
||
|
|
9da51cb507
|
chore: bump agents sdk to v3.6.15 (#15140) | ||
|
|
f3c6e24f84
|
⚡ perf: Cut Serial Round Trips and a 100ms Admission Stall from Chat Turns (#15138)
* ⚡ perf: Stop Awaiting the Conversation Access Marker Write Without Redis the CONVO_ACCESS violations namespace is backed by keyv-file, whose debounced write resolves after ~100ms. validateConvoAccess awaited that write before calling next(), so the first message to any existing conversation waited ~100ms before the request was even admitted — once per conversation per ten-minute window, on every default deployment. The marker only short-circuits the next check, so the write no longer gates the request. The same read now stashes the full document on req.resolvedConversation (null when absent) for downstream consumers. First-turn ack on an existing conversation: 109ms -> 5ms. * ⚡ perf: Read the Conversation Once per Chat Turn A chat turn read the same conversation document four times: the access check (two fields), the subagent thread guard (full document), agent initialization (the files field), and the first save. The access check now reads the full document and leaves it on req.resolvedConversation, the guard accepts that pre-resolved document instead of re-reading, and initializeAgent takes the conversation's file refs from it rather than issuing a separate findOne. Two serial round trips removed from every turn; the same document still serves the first save as before. * ⚡ perf: Remove Duplicate JWT Authentication on Agents Routes routes/agents/index.js applies requireJwtAuth and then mounts the v1 router at '/', which applied requireJwtAuth again. Every request through the agents router — chat turns included — ran the passport strategy twice: two signature checks and two user document reads. The v1 router is mounted nowhere else; its separately exported avatar router carries its own auth in files/index.js. * ⚡ perf: Skip the History Read for Root-Parent Turns and Walk the Tree in O(n) loadHistory fetched every message in the conversation and then walked the parent chain from the request's head. For a new conversation — or a new branch from the root of an existing one — the head is the root sentinel, which no message carries as its id, so the walk was empty by construction and the fetch was wasted. It now returns early. getMessagesForConversation found each ancestor with Array.find inside the walk, O(n^2) on a linear conversation (~5ms at 1000 messages). A Map by messageId makes it O(n); first-match semantics are preserved. |
||
|
|
b40fcb4c5a
|
🔥 chore: Bump @librechat/agents to v3.6.14 (#15135)
|
||
|
|
1d9c2fc591
|
🪴 feat: Fork Completed Subagents Into Continuable Chats (#15133)
* feat: continue completed subagents as chats * chore: sort continuation imports |
||
|
|
77cb72e50c
|
🧮 perf: Enable Agent Context Count Reuse (#15130)
* perf: enable agent context count reuse * fix: declare token counter return type * fix: initialize cached token counters * style: sort token counter imports * fix: Keep cached token counts exact |
||
|
|
8969ee4b18
|
🎚️ feat: Configure Agent Event Runtime in YAML (#15128) | ||
|
|
dd146ff74d
|
🧾 fix: Report Complete Agents API Usage (#15127)
* fix: report complete agents api usage * fix: preserve invoked usage context * test: cover absent usage context * fix: type responses usage finalization * fix: preserve reasoning usage aliases * fix: declare reasoning usage alias |