Commit graph

5307 commits

Author SHA1 Message Date
Danny Avila
b0f61d06fe 🗂️ fix: Resolve Code Interpreter Destination Collisions
Codeapi mounts each `/exec` input at a destination derived from its `name`
and rejects the whole request when two entries land on one. LibreChat never
enforced that: `primeFiles` sent `name: file.filename` verbatim, and the
graph seed deduped by `(storage_session_id, id)` — identity, not destination.

Two conversation files sharing a filename therefore killed code execution for
the rest of the conversation. The rejected request never reaches the sandbox,
so no result comes back to collapse the pair, and every later turn re-primes
both and fails identically. Only code-generated outputs are covered by the
`(filename, conversationId, context, tenantId)` partial unique index; uploads
carry `context: message_attachment`, so one conversation can hold several
records sharing a filename — two uploads of `image.png`, or an upload whose
name a later execution wrote back in place.

`packages/api/src/files/code/destinations.ts` now owns the name → destination
mapping for every contributor to an `/exec` file list, mirroring codeapi's
conflict rule including directory-prefix conflicts:

- `primeFiles` claims per pushed file, disambiguating with a `-2`, `-3`
  counter and echoing the resolved path into the tool context, so the model
  is told where each file actually mounts. Claim order comes from immutable
  `createdAt`, not `getFiles`'s `updatedAt` sort, which usage accounting and
  re-upload both bump — order-dependent destinations would silently repoint
  a path an earlier turn had already handed the model. Newest wins the bare
  name, matching `ToolNode.updateCodeSession` and giving an in-place rewrite
  precedence over the original it superseded.
- `seedCodeFilesIntoSessions` drops a later file whose destination is already
  claimed. Renaming there would invent a path nothing told the model about,
  and a conflict at that layer means one logical file arrived twice under
  different storage pointers.

Fixes #15443
2026-08-31 23:46:45 -04:00
dymux
22a9368604
🛄 fix: Refresh Stale Skill File References (#15413)
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 lc-dev (linux/amd64) (push) Waiting to run
Docker Dev Branch Images Build / Build lc-dev-api (linux/amd64) (push) Waiting to run
Docker Dev Branch Images Build / Build lc-dev (linux/arm64) (push) Waiting to run
Docker Dev Branch Images Build / Build lc-dev-api (linux/arm64) (push) Waiting to run
Docker Dev Branch Images Build / Merge manifests (lc-dev) (push) Blocked by required conditions
Docker Dev Branch Images Build / Merge manifests (lc-dev-api) (push) Blocked by required conditions
* fix(agents): check skill version in codeEnvRef cache-hit path

When a SKILL.md body is edited, `updateSkill` bumps `Skill.version`
but does not clear the `codeEnvRef` on bundled skill files. The
cache-hit path in `primeSkillFiles` reused those stale refs without
comparing versions, causing Code API to return 403 ("Unauthorized file
reference") because the session key no longer matched.

Add a version check to the cache-hit guard: a ref whose version does
not match the current skill version is treated as a cache miss, forcing
a fresh upload under the new session key.

Closes #15275

* fix(agents): apply version check to aggregate cache path in primeInvokedSkills

The previous commit added a version check to the per-skill cache-hit
path in primeSkillFiles. Codex review pointed out that
primeInvokedSkills has its own aggregate cache-hit branch that returns
cached refs before delegating to primeSkillFiles. That branch checked
ref presence and session activity but not ref.version against
skill.version, so a stale ref from an older skill revision would still
bypass the guard.

Add the same version comparison to the aggregate filter and include the
skill object in the allResolved entries so the version is accessible.

Addresses Codex review on #15413

* fix(agents): type skill cache version checks

---------

Co-authored-by: dymux <putramkti@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-08-31 22:32:20 -04:00
Danny Avila
a1e92e5530
🧩 ci: Scope Each Image's Digest Artifacts to Its Own Manifest Merge (#15444)
The first native-ARM run published `lc-dev-api` correctly but failed merging
`lc-dev`:

  Merging 4 platform digests: 18f83e4a... b4700f27... ca747c51... edf23a05...
  ERROR: ghcr.io/danny-avila/lc-dev@sha256:ca747c51...: not found

`lc-dev` is a prefix of `lc-dev-api`, so the merge job's
`pattern: digests-lc-dev-*` matched all four uploaded artifacts — both images'
digests — and then tried to resolve an API digest against the app repository.
`lc-dev-api` was unaffected because no other image name prefixes it.

Two changes:

- Artifact names carry an explicit `-arch-` separator, so one image's pattern
  can no longer reach another's digests.
- The merge asserts an exact digest count rather than merely rejecting zero.
  The old guard only caught an empty set, which is why four sailed through. Too
  few digests would publish a manifest missing an architecture; too many means
  the glob crossed images. Either is now a hard failure before anything is
  tagged.

No bad manifest was published: `imagetools create` failed while resolving its
sources, so `lc-dev` kept pointing at its previous build. The run also confirms
the approach itself works — `lc-dev-api` merged to a proper index carrying
linux/amd64 and linux/arm64, and the native arm64 legs built in 3-4 minutes
against 13+ minutes under QEMU.
2026-08-31 22:29:33 -04:00
Danny Avila
283aae49b3
🖼️ fix: Give a Phase's Files Their Own Media Row (#15435)
Some checks failed
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 lc-dev (linux/amd64) (push) Waiting to run
Docker Dev Branch Images Build / Build lc-dev-api (linux/amd64) (push) Waiting to run
Docker Dev Branch Images Build / Build lc-dev (linux/arm64) (push) Waiting to run
Docker Dev Branch Images Build / Build lc-dev-api (linux/arm64) (push) Waiting to run
Docker Dev Branch Images Build / Merge manifests (lc-dev) (push) Blocked by required conditions
Docker Dev Branch Images Build / Merge manifests (lc-dev-api) (push) Blocked by required conditions
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Docker Dev Images Build / 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
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
* 🖼️ fix: Give a Phase's Files Their Own Media Row

* ✂️ refactor: Drop the Inline-Media Dedup

Six review rounds produced thirteen findings. The media row accounts for
one and inline resolution for five, all fixed — both reason about
attachments, and both converged. The dedup accounts for seven, arriving at
roughly one per round (code fences, backtick spans of two widths, escapes,
indented blocks, invalid title syntax, and raw HTML blocks), because it is
the only part that has to know what markdown RENDERS. Each miss claimed a
file nothing displayed and took it out of the row, so the standing cost of
keeping it was a recurring way to hide content, in exchange for avoiding a
duplicate.

So it goes. `collectInlineMediaNames`, the fence tracking, the standalone
image-line whitelist, and the claim wiring in `ContentParts` are deleted,
along with the raw-HTML finding, which no longer has anything to apply to.

Both reported problems still ship. The phase card's files ride their own
row outside the fold, and a bare `![DTI](5_dti.png)` still resolves to the
file the run produced. The only loss is that a chart the model inlines now
appears twice — once in the row, once where the model put it — which is
noise rather than a defect, and cheap next to a file that appears nowhere.

What remains reasons only about attachments: which stored file a name
refers to, whether that name is ambiguous, and which paths this app
serves.

* 🧹 style: Restore import order after the media context import shortened

* 🪞 fix: Address Codex Round 7 — Latest Record and Dimension-Free Fallbacks

The name map kept the FIRST record of a file while `collectPhaseAttachments`
had been changed to keep the latest — the row and the inline reference
disagreeing about which record of one file is current. A tool rewriting a
path reuses its `file_id` and the later record carries the newer URL, so
the inline reference resolved to a stale one. Both surfaces now take the
latest, matching the attachment renderer.

Resolution was gated on `isImageAttachment`, which additionally demands
width and height. Those exist so `<Image>` can reserve layout space; a
markdown `<img>` needs neither. The download fallback an oversized output
produces carries an image name and a working URL and displays perfectly
well, and requiring dimensions dropped it — leaving `![chart](chart.png)`
broken for exactly the outputs big enough to matter. The gate is now what
an `<img>` actually requires: an image extension and a fetchable path.

That predicate also moves INSIDE `buildAttachmentsByName`, where the
contract belongs, rather than being a filter the one caller had to
remember. Reusing `isImageAttachment` was the wrong kind of DRY — it
answers a different question than the one asked here.

* 🧬 fix: Address Codex Round 8 — Run-Step Identity and the Recovery Renderer

Two of three; the artifact-panel one is deliberately left, see the PR
thread.

The row's dedup key omitted `stepId`, but `filterAttachmentsForPart`
routes records to their owning part BY run step — so two executions that
reuse a provider tool-call id and claim the same `file_id` are two
artifacts, and collapsing them dropped one from the row that, once the
fold hides the parts' own copies, is the only place it appears.
`attachmentRenderKey` adds that dimension on top of `attachmentIdentity`
rather than changing it: the merge in `useAttachments` reconciles DB rows
against live records whose `stepId` may not have arrived, so it must stay
tolerant of a field one side can lack. A step-less legacy row keeps the
plain identity and never splits from itself.

`MarkdownErrorBoundary`'s fallback renderer omitted the attachment-aware
`img`, so a generated reference reverted to a page-relative URL precisely
in the recovery path — where the media context still holds the file and a
broken image is least welcome.
2026-08-31 22:05:27 -04:00
Danny Avila
d3ea6f415d
🐆 perf: Build Dev Branch Images on Native ARM and Cheapen Asset Brotli (#15441)
The dev-branch image build takes 20-40 minutes and has hit its 130-minute
timeout. Neither cause was in the bundler.

Per-step timings from run 33424998112 show QEMU is ~84% of the work:

  npm ci             52.6s (amd64)  vs  333.2s (arm64, QEMU)  — 6.3x
  npm run frontend   90.4s (amd64)  vs  442.8s (arm64, QEMU)  — 4.9x

So build each platform on a runner of its own architecture, push by digest,
and stitch the digests into the multi-platform manifest lists in a merge job.
The tags consumers pull are unchanged. Layer caching goes to GHCR beside the
image rather than `type=gha`: the Actions cache is capped at 10GB repo-wide
and is branch-scoped, so `dev` and `main` runs would not share it, while a
registry cache has no cap and is reusable by the other four image workflows
when this pattern is rolled out to them.

Separately, vite-plugin-compression2 defaults to brotli quality 11, which
costs ~13s of CPU on this 18.4MB bundle against ~0.47s for gzip-9, and the
plugin's scheduler serializes quality >= 10 as a high-memory operation so it
cannot use the runner's cores. Quality 5 compresses in ~0.2s and still lands
under gzip (4.3MB vs 5.0MB). `.br` is only served when
ENABLE_STATIC_ASSET_BROTLI is set, which is off by default, so the 0.5MB the
top quality buys is paid for on every build of every platform and rarely
collected. The client build goes from ~11s to ~4s; gzip output is unchanged.

This is a pilot on the dev-branch workflow only. dev-images,
dev-staging-images, main-image-workflow and tag-images still use QEMU and
still have no layer cache.
2026-08-31 22:05:13 -04:00
Danny Avila
bd24e45489
🧵 refactor: Give the Subagent Panel One Composer and Main Chat's Metrics (#15436)
* 🧵 refactor: Give the Subagent Panel One Composer and Main Chat's Metrics

The panel drifted from main chat in the small measurements a reader
notices side by side, and its footer swapped controls under the pointer
every time a run settled.

- One persistent composer. `@librechat/client` gains a `Composer`: the
  shared composer surface with a field, a height-holding action row, and
  the main chat send affordance. The panel renders it for the whole life
  of a run — Enter steers while the child is live, and continues the
  thread once it has settled — instead of replacing the input with a
  "Continue this chat" button at completion. Its placeholder is the
  main chat one (`Message {agent}`), not bespoke guidance copy.
- Continuations carry the draft. Child threads are view-only server-side
  (`CHILD_THREAD_READ_ONLY_ERROR`), so continuing forks into a real
  conversation; what the reader typed rides along through the same
  per-conversation draft store the composer restores from, and comes
  back in the panel if the fork fails.
- Main chat's footer metrics. Turn footers use `messageFooterClasses`
  and hold their height, so the elapsed timer is sized by the `text-xs`
  its main chat counterpart inherits rather than the panel's body size,
  and the reading leaving at completion no longer steps the thread.
- The author column matches. Turns render `MessageIcon` unconditionally,
  and the streaming dot is inset onto the header icon's axis with
  `underHeaderIcon`, as main chat insets it.
- Status indicators pin their color. Label first, dot last, so a longer
  status grows inboard and the color lands on one column in every row; a
  live row shimmers its label the way a running tool call does, and the
  dot no longer pulses against it.
- The actor picker is the agent builder's. `ControlCombobox` with agent
  avatars and search, replacing a bespoke bare `Select`.

* 🧷 fix: Keep Enter Off an Empty Field and Drop Dead Actor Rows

- The shared `Composer` never submits an empty field on Enter. Continuing
  a settled thread needs no text, so its send button stays live — but a
  stray Enter must not navigate the reader out of the panel.
- The actor picker lists only actors with a task to open. Its list has no
  disabled state, so a child without one was a row that silently did
  nothing; the selected thread stays listed regardless.
- The header glyph loses its plate, like the `MessageRow` author slot, so
  an agent avatar reads as an avatar rather than an inset badge. The send
  glyph matches main chat's at 24px.

* 🎚️ fix: Hand Continuation Text Over in Memory, Not Through Saved Drafts

Codex review of the first pass, all three findings.

- The continuation's text no longer goes through the composer draft store.
  Draft restoration is gated on the Save Drafts preference, so with drafts
  off the forked chat opened empty and the words were left sitting in local
  storage the reader had asked not to use. A new transient
  `pendingComposerTextByConvoId` carries them in memory instead, keyed by
  conversation so the handoff survives a navigation that resolves its
  record before moving the route; `useTextarea` drains it once, after the
  autosave restore it must not lose a race with.
- The draft is read when the fork lands, not when it was requested, so
  anything typed while the request was in flight travels too.
- Actors whose task id has not arrived are left out of the picker rather
  than offered as rows that cannot open anything (already in the previous
  commit; the filter keeps the selected thread listed regardless).

* ⌨️ fix: Give the Panel Composer Main Chat's Keyboard Contract

Codex round two.

- The composer honors "Press Enter to send". With the preference off a bare
  Enter is a line break and never reaches the field's action, so reaching
  for one cannot steer a live run or leave for a continued chat; ⌘/Ctrl+Enter
  submits there instead.
- The IME guard covers Safari. `isComposing` is already false on the very
  Enter that commits a candidate there, so tracked composition state and the
  legacy `Process`/229 signals stand in for it, as the main chat composer's
  own guard does.
- The actor picker renders in the panel rather than a portal. On mobile the
  panel is a modal whose focus trap only knows its own descendants, so a
  portaled search field let Tab escape to the page behind it.

* 🧭 fix: Bind the Continuation Draft, Contain Escape, Defer to the Key Policy

Codex round three.

- The continuation draft is bound to the selection that asked for it. The
  composer is shared across selections, so a fork that lands after the
  reader moved on was handing over the words now in front of them and
  clearing the composer they belong to. The live field is still preferred,
  but only while it is still that continuation's own.
- Escape is handled on the panel element instead of through the focus
  trap's callback. That callback runs on a native listener on the aside,
  so it fired BEFORE any React handler inside it and closed the whole
  panel — taking the composer's text — when a nested popover meant to
  dismiss only itself. As a React handler it bubbles in DOM order, so the
  actor picker can contain its own Escape; `ControlCombobox` gained an
  `onOpenChange` so the panel knows when that is.
- The composer defers to the host's key policy. `Composer` takes an
  optional `resolveKeyVerdict`, and the panel routes it through the same
  `resolveComposerKeyDown`/`useComposerBindings` the main chat form uses,
  so a rebound or unbound submit shortcut behaves identically here and
  chords claimed by global shortcuts are left for the window handler.

* 🔑 fix: Stop the Trap Eating Escape, Freeze the Actor Mid-Continuation

Codex round four.

- `useFocusTrap` only consumes Escape when it actually answers it. Its
  listener is native and sits on the container, so it runs before every
  React handler inside — and by calling `preventDefault` with no callback
  it swallowed the key from the host that had taken Escape back, leaving
  the panel unclosable. Both other callers pass a handler, so nothing else
  changes. The panel suite now runs the REAL trap: stubbing it is what let
  the previous Escape assertions pass in both directions.
- Nothing swaps the composer while a continuation is out. The panel holds
  the draft that travels with the fork, so the actor picker is frozen and
  the newest-task advance defers until the request settles, rather than
  leaving another actor's words to be discarded with the panel.
- The 4 KiB cap applies only in control mode. It bounds a subagent control
  command's payload; a continuation is ordinary chat text bound for a
  composer that caps nothing.

* 🪢 fix: Hold the Selection While a Failed Continuation's Words Survive

Codex round five.

The newest-task advance deferred while a continuation was in flight, but
the error path released that hold at once — so the advance fired, the
control identity changed, and the identity effect wiped the composer. The
panel stays open after a failed continuation precisely so the reader can
retry with those words, and this threw them away.

The hold now covers a failed continuation for as long as its draft is
still in the box, and releases itself the moment the draft is sent or
cleared. The test asserts both halves, and fails if the hold narrows back
to the in-flight case.

* 🧹 fix: Hold for Any Unsent Draft, and Let a Cleared One Stay Cleared

Codex round six.

- Unsent words hold the selection wherever they came from. Protecting the
  draft only once a request had started or failed left the plainest case
  open: type a continuation, let a newer task arrive, and the advance
  changed the control identity and emptied the composer. The hold is now
  `continuationPending || draft is non-empty`, which subsumes the failed
  case and lets the extra flag go.
- Emptying the field after asking to continue is the reader withdrawing
  the words. While the composer still belongs to that continuation it IS
  the draft, verbatim, so the destination no longer reinstates text they
  deleted; only a composer that has moved to another selection falls back
  to what was captured at submission.
2026-08-31 21:46:58 -04:00
Danny Avila
47dbcb4910
🎯 fix: Keep the Thinking Label on the Lightbulb's Axis While Shimmering (#15437)
The shimmer PR (#15431) nested the label inside a span carrying `.shimmer`,
which is `display: inline-block`, alongside `truncate`. An inline-block whose
overflow is not visible takes its baseline from its bottom margin edge rather
than from its last line box, so the label row's line box grew from 18px to 23px
and the text lifted 2px while the lightbulb — pinned to the row's 30px centre by
the copy button — stayed put. Every streaming reasoning header sat off its own
icon's axis until the label settled back to "Thoughts".

The sweep now rides the label row itself. That row is a flex item, so
`.shimmer`'s `inline-block` is blockified away and the text lands exactly where
the un-shimmered label lands: measured against the real component, both states
report an 18px box and the same text centre, where the nested form reported
23px and a 2px offset.

The nested span survives only for the generated-label entrance, which cannot
share an element with the sweep because both drive `animation-name`. It keeps
the entrance outside — the clipped element paints the glyphs itself, so a
descendant's opacity cannot fade them — and takes `align-top` so the baseline
rule above cannot reopen the gap there.

Found by rendering the real component in a browser; a hand-built reproduction of
the same classes did not show it, and neither did the DOM assertions, which is
why the regression shipped.
2026-08-31 21:15:28 -04:00
Danny Avila
cceb936655
🪗 feat: Fold a Live Run's Tool-Call Groups Into the Phase Card (#15434)
* 🪗 feat: Fold a live run's tool-call groups into one phase card

A long agent run renders one row per labeled tool block, so a turn that
takes twenty steps pushes twenty disclosure rows between the reader and
the answer. The parent phase card already solves this — it just does not
exist until the server has summarized the phase, which is after the run.

Give `groupActivityPhases` the whole partition instead of only the part
the server has claimed: an unclaimed run of two or more labeled activity
blocks becomes a phase segment with a client-built header. `ContentParts`
renders `ActivityPhaseGroup` for it exactly as it does for a real phase,
so both cards share one definition of the chrome, the fold animation, the
lazy body, and the approval retention — change the styling once and both
move.

The header carries the newest filled child label, which makes the
collapsed row a ticker: it reads as the line that would have been at the
bottom of the unfolded list, and the generated summary replaces it
verbatim when the marker lands. The fold ends at that newest label, so
the reasoning and tool call still streaming stay outside the card where
they can be watched, and join on the commit their own label fills.

Boundaries mirror the server's own phase rule (steer, existing marker,
substantial prose) so the synthesized span and the phase card that
replaces it cover the same content. Answer text is always a boundary —
`groupSequentialToolCalls` absorbs only `commentary`, and nothing the
reader came for may end up behind a disclosure.

Toggle memory moves into the message-wide map both cards now share with
`ToolCallGroup`, keyed on the span rather than either header, so opening
the ticker to watch survives the swap to the summary.

The rule is content-derived, so history and a live run partition
identically and nothing unfolds at settle — `activityPhaseLabel` is
opt-in, and a deployment with child labels but no phase summaries would
otherwise drop the whole list back open at the end of every turn.

Parallel content stands down: those columns own their layout and a
synthesized card would pull them onto the phase path.

* 🎞️ fix: Do not replay the entrance fold when a summary claims a fold

A server marker landing on a span a synthesized card already covers was
reading as an arrival, so the real phase card mounted with the entrance
animation — which mounts a card OPEN and folds it shut over the next two
painted frames. On a span the reader had already watched fold, that
flashes every sub-group back onto the screen before collapsing again.

Record the content indices sitting behind a synthesized card and suppress
the entrance when the marker's span overlaps them. Indices rather than
keys, so a start boundary the server resolves slightly differently from
the client still counts as the same span.

* 🙋 fix: Never fold a span holding an unresolved approval

A fold summarizes work that is done. An approval waiting on the reader is
the opposite: the run is blocked until they answer, and burying the
request behind a disclosure they have to discover strands the turn.

A server marker cannot reach this state — it is emitted after the batch
resolves — but a subagent nested inside an already-labeled group can
raise an approval long after its parent's label filled, which puts it
inside a synthesized span. Render that span exactly as it would render
without the feature until the approval clears, then fold.

* 🧪 test: Assert the fold header ticks forward without re-opening

* 🧷 fix: Tighten what a synthesized fold may claim and hide

Four issues from review, each now pinned by a test that fails without its
fix.

An error, an image, or a summary between two labeled blocks was joining
the run and disappearing into the card. Membership is now the allowlist
`groupSequentialToolCalls` uses — tool calls, reasoning, commentary,
labels — plus AGENT_UPDATE, which the server deliberately keeps inside a
phase because a transfer card cannot join a tool group. Anything else is
content in its own right and ends the fold.

Orphan labels were counting as activities. A label that claims nothing
heads no group and renders as a standalone line, so two of them are not
two activities — folding them hid the first behind a card whose body was
just the pair of lines. Labels now count only when they claim a non-empty
block, with the claim closing at every label (blank reservations
included) exactly as `claimStart` does, and handoff calls claiming
nothing.

Card identity was anchored to the span's first part, so it moved when the
server extended a phase back across short unphased text the client treats
as a boundary — dropping the reader's open card on the swap the shared
identity exists to survive. It is an ordinal now: phase cards only ever
append in content order.

The synthesized entrance is gone. It mounts a card OPEN and folds it
shut, and this component remounts mid-run — `messageId` is in the key and
changes when the placeholder hydrates to the server id, and a reconnect
mounts straight onto existing labeled content — so it would flash the
whole fold back open partway through a run. Fold GROWTH was never
animated, so formation matching it is the consistent behavior.

* 🔑 fix: Key a phase card by its span so the summary reconciles in place

Two more from review, and the first one condemns the mechanism rather
than the instance.

Ordinal card identity was not stable: an earlier server phase can hold a
pending marker that yields no segment yet — its activities are handoffs,
or no child label has filled — so a later fold takes `phase:1` and is
pushed to `phase:2` when that marker fills, handing the reader's override
to a card they never opened. Any counter has this shape, so the transfer
map is gone entirely.

What replaces it needs no state to hand over. Both card kinds now render
under one key anchored to the SPAN, so the card that carries the summary
is the same element as the fold it replaces and React reconciles it in
place — the reader's open card simply stays open. A phase with no
children has no span and keeps its marker key. `ActivityPhaseGroup` is
byte-identical to dev again: the shared component needed nothing.

The honest limit, now pinned by a test: when the server extends its span
back across short unphased text the client treats as a boundary, the two
spans differ, and a card covering different content is keyed and mounted
as a different card. The toggle does not carry there. Keeping the
stricter boundary is deliberate — `phase` is only stamped by providers
that expose it, so matching the server would let a short final answer
hide inside a fold.

Second: `tool → transfer_to_* → label` was counting as a claimed batch,
while the grouping flushes the block at the non-groupable transfer and
then drops the label through `coversTransferCall`. Two such batches built
a card over labels that render nowhere. The claim now resets at any part
the block cannot hold, matching the flush.

* 🗝️ fix: Drop messageId from the card key and let a grown span animate

Two more from review; the second deletes a mechanism the span-keying had
already made redundant.

The assistant streams under a synthetic `${userMessage.messageId}_` for
the WHOLE run and only takes its server id at `finalHandler`, so a key
carrying `messageId` remounted every phase card at settle and dropped
whatever the reader had open — a guaranteed loss at the end of every
turn, not a narrow window. The card key is the span alone now, which is
how `ToolCallGroup` has always keyed for the same reason.

`replacesFold` suppressed the entrance whenever a marker's span merely
OVERLAPPED a previous fold. When the server extends its span back across
short unphased text the two spans differ, the card is genuinely a new
one, and suppressing the entrance made the text the reader was looking at
blink out of existence instead of being folded in. The suppression is
gone: a same-span replacement is now reconciled rather than remounted, so
it cannot animate anyway, and that was the only case worth suppressing.

With it goes the `folded` index tracking it was the sole reader of.
Anchoring identity to the span turned out to subsume both the expansion
map and this set — the diff is smaller than it was two rounds ago.

* 👥 fix: Separate a sibling switch from a message taking its server id

Dropping messageId from the card key fixed settle but opened the case it
had been guarding: MultiMessage renders siblings without a key and reuses
this instance, so an index-anchored card carried one response's open
state into an unrelated one.

Neither prop tells the two apart on its own — setIsSubmitting(false) is
batched with the final message swap, so that commit looks exactly like a
sibling switch by message identity. `siblingIdx` does: it moves when the
reader pages to another response and holds still when the same response
takes its real id.

* 🪪 fix: Pair the card's position with its first provider id

`siblingIdx` was the wrong discriminator. MultiMessage recomputes the
viewed response's positional index whenever a sibling is added or dropped
around it — its own docblock calls this out — so a key carrying it
remounted the card during churn that deliberately PRESERVES the response
the reader is on. Its docblock also records why no message id works
either: messageId and parentMessageId both move through the SSE
lifecycle, which is exactly why that component reconciles positionally.

The span's own position was never the problem — it is content-derived, so
churn cannot touch it. It only failed to separate two siblings whose
cards start at the same index. Pairing it with the first tool call's
provider id fixes that and nothing else: settle and churn leave both
halves alone, and the pair also keeps repeated provider ids in one
message apart, which a bare id would have collided on — the existing
`counts repeated legacy provider-id groups` case proved that immediately.

* 🧲 fix: Anchor the card to its first tool call and spare hoisted output

Anchoring the key to the span's first PART broke at settle whenever a
phase opened with an empty TEXT or THINK: preserveStreamedContentIdentity
drops those slots and stamps the survivor, so the anchor moved and the
card remounted exactly when the run finished. `getToolGroupId` already
records the answer — the tool calls themselves do not move — so the
anchor is the first tool call's own position now, with its provider id
still alongside to keep repeated ids apart.

That also closes the limitation this PR had been documenting. A phase the
server extends back across short unphased text still shares its first
tool call with the fold, so the two now reconcile and the reader's open
card rides through. The test that pinned the old degradation asserts the
better behavior instead.

Second: `ToolCallGroup` renders `groupAttachments` OUTSIDE its own panel
so a generated image or file survives collapsing the group. Folding that
group into a card put the hoist back inside a disclosure and the output
disappeared. These attachments are message-level and never appear in
`segment.content`, so the visible-part allowlist could not catch them —
a span carrying any takes the same escape as an unresolved approval and
renders unfolded. Hoisting them to the card would be better and would fix
the same hole for server phases on dev; it is not this PR.

* 🎯 fix: Give id-less spans a scope and stop the second streaming cursor

Two from review.

The card key's no-provider-id fallback was a bare position, which cannot
separate two siblings whose cards start at the same index — the exact
hole the provider id had just closed, reopened for id-less legacy calls.
That branch carries the message-change counter now, which is what
`getToolGroupId` does for its own id-less fallback, and it accepts the
same tradeoff: the counter advances at settle, so those cards remount
then. Spans that have a provider id are unaffected.

`showEmptyCursor` never consulted `isLast`, so a trailing empty provider
slot rendered the initial waiting state from inside its own segment even
though a card was already carrying the cursor — two dots at once. Reusing
`isLast` would have been wrong: it means "leaf of the tree", and the
existing thinking-dot tests legitimately leave it false while submitting.
The phase branch now computes which card owns the cursor and passes that
down, alongside the other internal segment signals. It covers real phase
markers too, which had the same duplication.
2026-08-31 21:14:20 -04:00
Marco Beretta
146664e0d1
🔐 fix: Redirect OAuth Browser Navigations Instead of Rendering JSON (#15440)
* 🔐 fix: Redirect OAuth Browser Navigations Instead of Rendering JSON

The /oauth/* routes are reached by top-level browser navigation, so a
middleware that rejects one with a JSON body makes the browser render
that body as the page. Hitting the login rate limit during a social
sign-in dropped the user on a blank page showing:

  {"message": "Too many login attempts, please try again after 5 minutes."}

Mark those requests with markOAuthNavigation and have the two middlewares
that can reject them answer with a redirect to the login page carrying an
error code the client localizes. The login limiter stays a single instance
so the attempt budget is still shared with POST /api/auth/login, and that
route keeps its JSON 429 for the SPA.

* 🧪 fix: Address PR Review Bot Findings

chatgpt-codex-connector:
- Split the inline `type TranslationKeys` specifier out of the value
  import, per the standalone `import type` convention in CLAUDE.md. This
  also clears the Static checks import-sort failure.
- Cover the new OAuth error branches in Login.spec.tsx: each mapped
  `?error=` code raises its localized toast and the query is cleaned,
  and unmapped codes including prototype members raise nothing.
2026-08-31 21:13:47 -04:00
Danny Avila
49e1e32f3a
🧺 fix: Funnel MCP Catalog Work Through a Shared Gate (#15429)
* 🧯 fix: Bound MCP Catalog Work Across Requests

* 🛑 fix: Reject Saturated MCP Discovery

* fix: fairly bound MCP catalog work

* test: type catalog snapshot mock arguments

* test: assert snapshot deadline forwarding

* fix: harden MCP catalog scheduling
2026-08-31 14:13:31 -04:00
Danny Avila
2e2c29e10d
🫀 fix: Expire Aborted Background Tool Leases (#15428)
* fix: Bound Background Tool Producer Heartbeats

* fix: fence timed-out delivery retirement
2026-08-31 14:13:00 -04:00
Danny Avila
6be3f798fc
🪩 style: Shimmer Live Reasoning Labels and Timers (#15431)
* ✨ style: Shimmer the Thinking Label and Elapsed Timer While Generating

The label above streaming reasoning and the elapsed-time reading beneath a
streaming response both sat static while every other in-flight indicator on
the row — a running tool call's label, a running skill, a pending question —
carried the shimmer. Give them the same treatment so "in flight" reads the
same way everywhere.

Both are gated on state that is already established:

- `Reasoning` shimmers only while `effectiveIsSubmitting && isLast`. Settled
  thoughts, an older row the reader paged to, and reasoning overtaken by
  later content all keep the plain label. The legacy `Thinking` component
  and `ThinkingLabel` (the detached-subagent marker) do not opt in.
- `Elapsed` only ever renders under the streaming latest row at the newest
  sibling position (`shouldShowElapsed`), so there is no settled reading for
  the animation to misrepresent. Completed tool-call durations go through
  `getRunStepDurationLabels` and are untouched.

`.shimmer` and Tailwind's `animate-in` entrance both drive `animation-name`,
and `.shimmer` — plain CSS after `@tailwind utilities`, equal specificity —
would win and silently kill the generated-label slide-in. The two therefore
sit on nested spans, in the order that is safe for `background-clip: text`:
the transform on the ancestor, the clipped background on the descendant.

* 🎨 fix: Drive the Shimmer From Theme Roles and Honor Reduced Motion

Two review findings, both against the shared `.shimmer` utility rather than
this PR's call sites — but this PR adds two more consumers of it, one of which
sweeps continuously for the whole generation, so they land here.

Theme roles. The gradient hard-coded near-black and near-white and selected
the second with `.dark`, so a stored or custom theme that restated its text
colors got a shimmer that ignored them. Themes are applied as bare RGB
triplets on the document element; the sweep now reads `--shimmer-base` /
`--shimmer-dip` (with the established `-alpha` companions the border roles
already use) and falls back to `--text-primary` when a theme states no base of
its own. The fallback is resolved at the label, not in the `:root`
declaration — `var()` substitutes where it is declared, so seeding
`--shimmer-base: var(--text-primary)` on `html` would have frozen the default
and ignored every theme scope applied further down. Dark still declares its
base outright: that value is dimmer than the dark theme's text color, and
restating it as `--text-primary` would have brightened every shimmer.

Default light and dark are preserved: rendered old against new at a frozen
sweep position, 144 of 137,280 pixels differ by a single 1/255 channel step,
which is the alpha-syntax rounding path. A theme that sets only
`--text-primary` now colors the sweep with no shimmer-specific override.

Reduced motion. `.shimmer` was the one custom animation in `style.css` with no
`prefers-reduced-motion` guard — `.t-icon-swap`, `.t-avatar`,
`.scroll-animation-*` and `[data-lc-fade]` all carry one. Stopping the
animation alone is not a fallback: the background settles at position 0 and
shows only half the sweep, stranding the tail of the label in the
near-invisible dip. The guard drops the gradient and hands the glyphs back to
the color they would otherwise have had.

* 🎨 fix: Register the Shimmer Stops and Keep the Sweep Whole

Codex round 2, on the previous round's fixes.

Register the stops. Moving the constants into CSS variables did not make them
theme-owned: `applyResolvedTheme` writes only `themeColorTokens`, so nothing a
theme declares could reach `--shimmer-base`. Dark was the case that mattered —
`style.css` declares a `.dark` base outright, which a theme can only outrank by
having the property applied to the document element — so a custom dark theme
still got the stock white sweep. `rgb-shimmer-base` and `rgb-shimmer-dip` now
sit in the registry beside the text roles, seeded with today's values in both
modes; `registry.spec` already enforces light/dark key parity. The opacities
stay in CSS as `--shimmer-*-alpha`, the way the border roles keep
`--border-*-alpha`.

This narrows the earlier claim: a *versioned* theme resolves every default, so
it now states its own sweep rather than inheriting `--text-primary`. The
`--text-primary` fallback still serves plain CSS and legacy partial `themeRGB`
themes, which apply only the keys they name.

Keep the sweep whole. `key` remounts the label row so the entrance replays,
restarting the sweep with it; reasoning labels revise on a 3s default
(`DEFAULT_UPDATE_INTERVAL_MS`) against a 4s sweep, so it never completes a
cycle. Keying only when an entrance will actually play leaves an un-animated
label — smooth streaming off, or reduced motion — with a stable identity and an
unbroken sweep, instead of a bare snap with nothing to cover it.

Not adopted: keeping the shimmer element stable and replaying the entrance
inside it. The clipped element paints the glyphs itself, so a descendant's
opacity cannot fade them — rendered at entrance t=0, the current nesting is
correctly invisible while the swapped one sits fully lit through its own fade.
The remaining restart happens only underneath a deliberate entrance animation.

* 🎨 fix: Carry Legacy Themes Onto the Registered Shimmer Stops

Codex round 3. Registering the stops migrated versioned themes but not stored
or environment `themeRGB` ones: `ThemeProvider` routes those to the legacy
`applyTheme` adapter, which bypasses `resolveTheme` and applies only the keys a
theme names. A stored theme has no shimmer keys, so it kept the stock sweep
while the rest of its palette moved — and in dark the CSS cannot recover on its
own, because `.dark` declares a base outright and the `--text-primary` fallback
never runs.

`mapColors` already carries legacy themes across a token they predate:
`--surface-composer-hover` derives from `rgb-surface-hover` when absent. The
shimmer base follows the same shape, deriving from `rgb-text-primary` — which
is what it already resolves to in light, so only dark changes. The dip has no
legacy counterpart and stays at its default; it is the faded half of the sweep,
carried at low alpha, so it reads as dimmed against any base.

Not a regression being fixed but an improvement being extended: before this
branch, every theme got the same hard-coded sweep. This is the last path that
still did.

* 🎨 fix: Mirror the Shimmer Fallback Onto Versioned Theme Resolution

Codex round 4. The compat fallback added last round covered only the legacy
adapter, but this codebase carries these fallbacks on both paths: the
`rgb-surface-composer-hover` derivation exists in `mapColors` AND in
`resolveTheme`. Adding the shimmer stop to just one left the halves disagreeing.

The versioned path is the one where the omission actually hurts. A definition
written before these keys existed cannot name them, so `resolveTheme` filled
the gap from the bundled base and pinned that theme's in-flight labels to
LibreChat's own sweep — a theme restating its text as white would light every
label in the stock near-black. Omission there means "not written yet", not
"wants the bundled sweep"; a theme that does want it still says so by naming
the stop, exactly as it opts out of the composer-hover fallback.

Both paths now behave the same: a theme that restates its text and names no
shimmer base gets its sweep from that text color, whichever way it is applied.
2026-08-31 14:12:39 -04:00
Danny Avila
98eb3f8b1c
🧿 fix: Restore Shared IP Limits for Generation Retries (#15427)
* fix: Preserve IP Limits for Generation Retries

* test: Cover Scheduled Retry IP Limiting

* style: format retry limiter route
2026-08-31 14:11:12 -04:00
Danny Avila
982a9a5141
🧴 fix: Strip LangChain URLs From Provider Errors (#15421)
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: Strip LangChain Attribution From User-Facing Provider Errors

LangChain classifies provider failures by mutating `error.message` with a
docs URL, and that message is forwarded verbatim into the error content part
we persist, so users saw `docs.langchain.com/...` credited for a provider's
own 429. The client only intercepted `MODEL_NOT_FOUND`, leaving every other
code to render raw.

Resolve the classification server-side instead: codes with localized copy
become the typed payload the client already knows how to localize, and every
other message keeps the provider's wording with the docs URL removed before
it reaches the database. The untouched error still reaches the logs through
`getSafeErrorMetadata`.

The client keeps a code-driven lookup for messages persisted before this, and
gains `com_error_model_rate_limit` alongside the existing not-found copy. The
OpenAI-compatible and Responses controllers now share one provider-error
helper rather than three copies of it, so the strip applies on those surfaces
too.

* 🛡️ fix: Harden Error Sanitization For Non-String Messages And Legacy Text

Codex review follow-up.

`stripLangChainTroubleshootingUrl` assumed a string, so an error-like object
with a truthy non-string `message` threw `.replace is not a function` inside
the error handler that was meant to report it — the template interpolation it
replaced had coerced such values safely. It now coerces at the boundary, as
does the code lookup.

Legacy messages whose LangChain code has no localized copy still rendered the
docs URL, which is exactly the historical case the client-side path exists to
clean up. The rendered fallback is now stripped too.

Both helpers move to `librechat-data-provider`, which is shared by the client
and the backend, so one implementation serves both instead of a second regex
drifting in the component.
2026-08-31 12:51:34 -04:00
Danny Avila
f0f82ec5fa
🧷 fix: Fence Server-Admitted Queue Boundaries (#15393)
* fix: fence server-admitted queue boundaries

* fix: preserve effective queue admission boundaries

* fix: fence queue recovery predecessor

* fix: unify queued boundary reconciliation

* fix: make queued turn receipt settlement monotonic

* fix: narrow queued receipt projection status

* fix: close queued turn settlement races

* fix: make queued admission lineage clock independent

* fix: centralize queued turn admission ownership

* test: type queued admission outcomes

* fix: make queued admission identity durable

* fix: carry queued admission identity end to end

* style: sort queued turn test imports

* fix: unify indeterminate admission ownership

* fix: migrate queued admission fence state

* fix: migrate every queued admission owner

* fix: surface queued admission reconciliation

* fix: preserve queued receipt evidence

* test: type queued receipt regression

* fix: stabilize queued receipt authority

* fix: supersede initial receipt fetches
2026-08-31 12:31:07 -04:00
Danny Avila
f20b978075
🎯 ci: Retarget Pull Requests Opened Against main to dev (#15416)
* 🎯 ci: Retarget Pull Requests Opened Against `main` to `dev`

Contributions land on `dev`; `main` only moves at release time. With `main` as
the repository's default branch, pull requests default to it, and the mismatch
has been resolved by hand at merge time.

Adds a `pull_request_target` hook that switches a newly opened or reopened pull
request's base from `main` to `dev` and posts a one-time explanation, plus a
manual `workflow_dispatch` sweep (dry-run by default) for the existing backlog.

Release-bound upstream branches keep `main` as their base: `dev`, `release/*`,
`hotfix/*`, `backport/*` and `*-main`. The `target: main` label exempts any
pull request. The exemptions apply only to branches on this repository, so a
fork branch that happens to match one of those names is still retargeted.

The job checks out the trusted base commit rather than the pull request head,
so the write-scoped token never runs contributor code.

`pull_request_target` reads its workflow definition from the base branch, so
this must exist on `main` to take effect.

* 📝 docs: Record the `dev`-First Branch Policy for Agents

`CLAUDE.md` and `AGENTS.md` never stated which branch work targets, and the
default branch is `main` — so `gh pr create` and the GitHub UI point at `main`
unless told otherwise, and an agent following the defaults gets it wrong.

States the policy, the reason a backport to `main` is never needed (`main` is
kept as a strict ancestor of `dev`), and the exemptions the retarget workflow
honors.

* 📐 docs: Record Typecheck, Formatting and Issue-Closing Gotchas for Agents

Four repeat cycle-wasters that neither file stated, each verified against the
current tree:

- `tsdown` emits without typechecking, so `npm run build` proves nothing about
  types in `packages/api`, `packages/client` or `packages/data-schemas`; only
  `packages/data-provider` runs `tsc` in its build.
- `packages/client/tsconfig.json` excludes `*.spec.ts(x)` and `*.test.ts(x)`,
  so type errors in its specs surface only at run time.
- `npm run sort-imports` with no arguments rewrites all six source roots.
- `Fixes #N` never closes an issue here, because closing keywords are honored
  only on merges into the default branch and everything merges to `dev`.

Also notes that worktrees share one stash stack, and points at
`npm run static-checks -- --against origin/dev` as the local mirror of CI.

* 🔁 fix: Address Codex Review on the Retarget Workflow

- **Backport branches are no longer exempt.** Exempting `backport/*` and
  `*-main` contradicted the invariant the policy rests on: `main` is a
  fast-forward of `dev`, so a backport merged straight to `main` is precisely
  what breaks it. `target: main` remains the deliberate, explicit escape hatch;
  `dev`, `release/*` and `hotfix/*` stay exempt as release-bound branches.
- **`npm run update` does not sync `dev`.** `config/update.js` hard-codes
  `git checkout main` and `git pull origin main` — it is the self-host
  deployment updater, not a branch-sync tool. Telling contributors to refresh
  `dev` with it would have branched them off `main` and recreated the very
  flow this change exists to prevent. Replaced with the explicit git commands.
- **A dry run no longer reports work it did not do.** The summary said
  "Retargeted N" on a dispatch that wrote nothing, which could read as a
  completed sweep.
- **A failed explanation comment is recorded, not swallowed.** The base edit
  has already landed at that point, so a later run would skip the pull request
  and the contributor would never receive the note. It now counts, fails the
  run, and names the retry: `EXPLAIN_MISSING=true`, which posts a missing
  explanation onto an already-retargeted pull request.
- **The sweep paginates.** `gh pr list --limit 500` is a cap, not a page size,
  and would have silently ignored the remainder while reporting success.

Verified against the stubbed `gh` boundary: `*-main` now retargets, the dry run
performs zero writes and says so, a failing comment yields a non-zero exit, and
`EXPLAIN_MISSING` posts onto a pull request already based on `dev`. The
paginated query returns all 258 open pull requests based on `main`.

* 🧷 fix: Address the Second Codex Round on the Retarget Workflow

- **`already_explained` now separates "lookup failed" from "no marker".** A
  transient comments-API failure returned the same status as a clean miss, so
  the run would post a second explanation and report success — defeating the
  deduplication it exists for. A failed read now declines to post and is
  recorded.
- **`CONTRIBUTING.md` syncs from the canonical remote.** The previous fix
  pulled `dev` from `origin`, which in the fork-based flow described two
  sections later is the contributor's own fork — so it would fail outright or
  silently sync a stale branch, landing them on the wrong base again.
- **The hook retries on `synchronize`.** If the `opened` run failed before
  `gh pr edit` landed, nothing retried and the pull request stayed on `main`.
  A retargeted pull request no longer matches `branches: [main]`, so this adds
  no redundant runs — and the existing backlog now drains on contributor
  activity rather than only by sweep.
- **Sweeps serialize against each other** via a shared concurrency group, while
  the hook stays keyed per pull request. Deliberately not one global group:
  GitHub keeps a single pending job per group, so a burst of openings would
  cancel queued retargets — trading a rare duplicate comment for dropped
  retargets is the wrong way round.
- **`main` is an ancestor of `dev`, not a strict one.** Right after a sync the
  two refs are equal, which the previous wording described as a violated
  invariant.

Verified: a failing comments lookup edits the base but posts nothing and exits
non-zero.
2026-08-31 11:29:34 -04:00
Danny Avila
b7ae67307e
🕰️ fix: Anchor Temporal Variables to Turn Start (#15419) 2026-08-31 10:38:10 -04:00
Danny Avila
06ddaf8a3d
🧷 fix: Attach to a Continuation the Finished Run Left Behind (#15414)
* 🧷 fix: Attach to a Continuation the Finished Run Left Behind

The FINAL path never clears the submission atom. A pane that has just finished
a run therefore keeps holding it, and the external-run re-arm treated that as
"already attached" — so the announcement was skipped, the status query stayed
disabled, and nothing attached.

That is the common shape, not an edge case. A background tool dispatch
finishing starts the next generation for a conversation whose pane almost
always just completed the run that dispatched it. The sidebar spinner showed
the run because it reads the active list directly; the pane itself sat on the
previous turn until a reload, so both the harvested task output and every
following generation stayed invisible.

Distinguish an attachment from bookkeeping: a submission stands in for a live
stream only while `isSubmitting`, which stays true across reconnect backoff and
is raised before a submission is installed, so recovery still reads as attached
and a fresh send has no window where it looks finished. When the announcement
is answered and a finished submission is still installed, release it — the run
it describes is over, and a server-side continuation is a different generation
that the resume path owns building for.

* 🪢 fix: Treat an Attaching Resume as Live, Not as Leftovers

Self-review of the previous commit. Gating liveness on `isSubmitting` alone
reads a resumed attachment as finished for the whole window between installing
its submission and its stream opening — this hook installs the submission, and
only the `open` handler raises `isSubmitting`. An announcement landing there
would release the submission this hook had just built, tearing down the
attachment to rebuild it, and on a slow or retrying subscribe that repeats
every heartbeat.

The generation epoch is the missing half: this hook stamps it immediately
before installing a resume submission, `subscribeToStream` stamps it for every
other attachment, and terminal teardown clears it. Read both — `isSubmitting`
covers sends and reconnect backoff, the epoch covers attaching — and a
submission counts as leftovers only when neither says otherwise.
2026-08-31 10:20:33 -04:00
Danny Avila
b88cb3e361
👐 ci: Cancel the Playwright E2E Gate With Its Run (#15415)
The `e2e` fan-in gate guarded itself with `always()`, so a run superseded by
`concurrency: cancel-in-progress` still dispatched it onto a fresh runner after
every other job had gone `cancelled`. It then read
`needs.e2e_shards.result == 'cancelled'`, took the `!= 'success'` branch, and
exited 1 — leaving the required `e2e` check red on a run that was never
adjudicated, and burning a runner slot to do it.

`!cancelled()` keeps the property the gate actually needs, running when a shard
fails so the failure is reported rather than skipped, and drops the one it never
wanted. It also matches `e2e_shards` and `mcp_tool_list_changed`, which already
guard themselves this way; run 33391962579 shows both lanes running normally
past a skipped `codegraph_select`, so a skipped dependency still reaches the
gate.


Claude-Session: https://claude.ai/code/session_012jJbt9x1sCu8ggJQmdjoMy

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-31 09:54:53 -04:00
Danny Avila
81338cef95
🍃 test: Sweep Every DB Schema Method Against a Live Engine (#15392)
* 🍃 test: Sweep Every data-schemas Method Against a Live Engine

Adds `misc/documentdb/sweep.documentdb.spec.ts`: drives all 510 exported
data-schemas methods against a real engine and records, per method, how many
driver queries it issued and whether the engine rejected any of them.

Every DocumentDB incompatibility found so far — pipeline updates, `$$REMOVE`,
`$facet`, the transaction-probe false negative, the Mongoose-compiled mixed
projection — was invisible until someone thought to look for its class. The
sweep inverts that: the engine adjudicates whatever each method emits, known
class or not. Query shapes are parsed before they are matched, so an empty
database adjudicates most entry paths.

Mechanics: arguments are synthesized from each function's parameter names,
then repaired adaptively from the validation errors they provoke (ObjectId and
number casts, required schema paths, object-parameter shapes) until the method
reaches the database; the driver prototype is instrumented to count calls and
capture server errors — including on cursor consumption, where parse errors
actually surface — and duplicate-key violations are classified as benign. A
method that never issues a query is reported un-adjudicated instead of
silently green: coverage is visible, not assumed.

Two modes with identical mechanics so matrices diff cleanly: in-memory MongoDB
(`SWEEP_BASELINE=true`, free, CI-able) and live DocumentDB (`DOCUMENTDB_URI`).
`SWEEP_REPORT_PATH` writes JSON for machine diffing;
`DOCUMENTDB_STRICT=true` turns rejections into failures.

First full run against live DocumentDB 5.0.0: 363 of 510 methods issued at
least one query, zero engine rejections, zero divergences from the MongoDB
baseline. The jest config gains the main config's transform-ignore patterns so
the full method bundle (which pulls ESM markdown tooling) loads.

* 🍃 fix: Adjudicate Index DDL, Real ACL Cascades, and a Run-Scoped Sweep Database

Review round 1, all four findings real:

- The driver instrumentation now wraps `createIndex`, `createIndexes`,
  `dropIndex`, `dropIndexes`, and `listIndexes` (cursor-wrapped), so an engine
  rejecting index DDL — which `recordAuditEntry` issues before any listed
  operation and then swallows — is adjudicated instead of surfacing as a
  false `no-queries` under STRICT.
- The `removeAllPermissions` no-op override is gone: `createMethods` supplies
  a real implementation delegating to `deleteAclEntries`, so driven deletion
  cascades exercise their ACL operations (not-driven drops 147 → 142).
- Each race timer is cleared when its attempt settles, instead of hundreds of
  referenced timers living until the shared deadline and the last one holding
  the process open after the report.
- The live run connects to a run-scoped database
  (`librechat_sweep_<runId>`), so its `dropDatabase` teardown can never erase
  fixtures out from under the sibling live suites sharing the URI, nor any
  retained state in the URI's own database.

* 🍃 fix: Attribute Straggler Queries by Async Context and Adjudicate Transaction Commits

Review round 2, all three findings real:

- Per-method attribution now rides AsyncLocalStorage instead of a module
  global: a method that outlives its timeout keeps recording queries and
  engine errors under ITS OWN row rather than corrupting whichever row the
  sweep advanced to, and a final reclassification pass upgrades any row whose
  engine error arrived after its outcome was assigned.
- The session prototype's `commitTransaction`/`abortTransaction`/
  `withTransaction` are instrumented: transaction-lifecycle rejections surface
  from the session, not a collection operation, and several methods convert
  them into domain errors — previously a real commit rejection classified as
  a mere `threw` and STRICT passed.
- The compatibility doc's published numbers now come from the
  corrected-harness baseline (368 driven / 142 un-driven) and state explicitly
  that the recorded live-run matrix predates the instrumentation widening, so
  the next live run is authoritative for the newly adjudicated surfaces.

* 🍃 fix: Settle Stragglers Before STRICT and Commit a Real Transaction on a Replica-Set Baseline

Review round 3, all four findings real:

- STRICT is now enforced against the FINALIZED rows: `afterAll` waits
  (bounded) for every timed-out invocation to settle, reclassifies rows whose
  engine errors arrived late, and asserts zero rejections — previously a
  straggler's rejection could arrive after its per-test assertion had already
  passed against the `timeout` outcome.
- The baseline runs on a single-node replica set with the live branch's exact
  connection options (`autoIndex`/`autoCreate` off, run-scoped `dbName`):
  a standalone baseline would reject driven transactions for topology reasons,
  and asymmetric model auto-creation would attribute DDL in one mode and not
  the other — both manufacturing false matrix divergences.
- `resolveMCPAuthorityProof` gets a seeded case (fixture ported from the
  compat suite, executed in tenant context via the new `within` wrapper on
  overrides): generic synthesis died on target validation before a session
  ever started, so the session instrumentation adjudicated nothing. It now
  drives 22 operations through the production path, transaction commit
  included, on both engines.

* 🍃 docs: Sync the Published Baseline Counts With the Seeded Harness

* 🍃 fix: Materialize Transaction-Read Collections and Settle Stragglers Before Teardown

Review round 4, three findings real (the fourth — stale doc totals — was
already fixed on the pushed head):

- Straggler settlement moved BEFORE engine teardown: dropping the database or
  closing the client would abort outstanding work with closed-client errors
  that are not engine verdicts, leaving a timed-out row unadjudicated while
  STRICT passes.
- The authority fixtures (sweep AND compat) materialize PluginAuth and Token
  before the transaction: `loadCurrentProof` reads both inside it, and
  DocumentDB rejects any in-transaction statement touching a non-existent
  collection — the rejection surfaces as `proof_unavailable` before the commit
  is reached. This is the mechanism behind the compat authority test failing
  on every live cluster run while passing against MongoDB; with the
  collections materialized, the next live run adjudicates the transaction
  itself instead of the missing namespace.
- Engine errors are recognized by `instanceof MongoServerError` instead of a
  `name` equality, so server-originated subclasses (MongoWriteConcernError,
  MongoBulkWriteError) can no longer slip through the session wrapper as
  domain throws.

* 🍃 fix: Materialize Group and Config for the Authority Transaction and Exclude Constructor Exports

Review round 5, both findings real. The proof transaction also reads Group and
Config; the sweep fixture materializes both (the compat fixture already
creates a Config record and now materializes Group). SessionError — an error
class exported alongside the session methods — is excluded from the method
map, correcting the totals to 509 methods / 369 driven / 140 un-driven; the
published baseline is synced in the same commit.

* 🍃 fix: Build a Fresh Method Bundle per Case and Emit a Normalized Matrix

Review round 6, both findings real:

- Every row now invokes a freshly constructed method bundle. Factory-local
  caches leaked across alphabetically ordered rows — the session methods cache
  `sessionIndexesPromise`, so `ensureSessionIndexes` returned that settled
  promise and was misreported as `no-queries`. It now issues its 3 index
  operations, making the matrix order-independent (370 driven / 139 un-driven).

- The report separates run metadata from a canonical `rows` payload with
  run-specific values normalized out (run id, generated ObjectIds and UUIDs),
  so the two engines' matrices diff cleanly instead of differing on every row
  that embeds a synthesized value in its error text.

* 🍃 fix: Materialize Authority Snapshot Namespaces Before Its Transaction

Amazon DocumentDB rejects any statement inside a transaction that touches a
collection which does not exist, and `asMCPError` converts that server
rejection into `proof_unavailable` — so on a deployment where no PluginAuth
or Token row has ever been written, every MCP authority proof fails with an
error naming nothing about the real cause. This is the mechanism behind the
live-harness authority probe failing on every real cluster run since July
while passing against MongoDB.

`loadAuthoritativeSnapshot` now materializes the nine namespaces its snapshot
reads before opening the transaction, once per process. MongoDB tolerates the
in-transaction read either way, so nothing changes there; a create failure is
logged at debug and left to surface from the transaction itself.

The regression test drops the credential namespaces to reproduce the state and
asserts the resolve recreates them and succeeds; it fails without the fix.

* 🍃 docs: Record the Authoritative Hardened Live Run

The hardened harness ran against a live DocumentDB 5.0.0 cluster: 370 of 509
methods drove at least one query, zero engine rejections, and the matrix is
byte-identical to the MongoDB baseline — every outcome and query count
matches. This run adjudicated index DDL, transaction commits and aborts, ACL
cascades, and the authority snapshot transaction, none of which the first
pre-hardening run could see.

The same run turned the compatibility suite's MCP authority probe green for
the first time on any real cluster, confirming the namespace-materialization
diagnosis for the `proof_unavailable` failures seen since July.

* 🍃 fix: Retry Snapshot Namespace Creation After a Transient Failure

The catch suppressed every `createCollection` error, so a transient failure
settled the once-per-process memo as successful: creation was skipped for the
rest of the process, the transaction kept failing on the missing collection,
and every later proof returned `proof_unavailable` even after the condition
cleared. The existing reset-on-reject path could never fire because the inner
promise never rejected.

Only `NamespaceExists` (including a concurrent creator winning the race) is
suppressed now; every other failure propagates, clears the memo, and lets the
next request retry. The regression test rejects the first creation with a
transient error, asserts the proof fails, then asserts the following request
recreates the namespace and succeeds — it fails against the swallow-all form.

* 🍃 fix: Throttle Snapshot Namespace Preflight Retries With a Bounded Cooldown

Clearing the memo on every rejection made a persistent failure — an
authorization error, say — turn each authority request into another serial
collection preflight: a DDL retry storm precisely while the database is
unhealthy, since the single-flight promise only coalesces requests while an
attempt is pending.

A failure is now recorded with its timestamp and replayed without touching the
database until a bounded cooldown elapses (30s, overridable through the
existing hooks for tests). Transient failures stay retryable, just not on
every request. Both sides of the boundary are covered: one test asserts a
persistent failure issues no further creations inside the cooldown, and the
existing retry test runs with the cooldown disabled to assert recovery still
works. The throttle test fails with the cooldown gate removed.

* 🍃 fix: Instrument Db-Level Namespace DDL in the Sweep

The authority path's namespace preflight calls `Model.createCollection()`,
which delegates to `Db.createCollection` — not to any `Collection` operation
the sweep wrapped. A DocumentDB rejection of that DDL would therefore be
laundered by `asMCPError` into `proof_unavailable`, leaving the row
classified as a mere `threw` while `DOCUMENTDB_STRICT=true` passed: the exact
false-green class this suite exists to close, reintroduced by the preflight
itself.

`Db.createCollection`/`dropCollection`/`renameCollection` are now
instrumented, and `NamespaceExists` (48) joins the benign server codes since
an idempotent preflight against an existing collection is the expected
outcome, not an incompatibility. The authority row's adjudicated query count
rises from 22 to 31, which is those nine preflight operations becoming
visible.
2026-08-31 09:54:32 -04:00
Danny Avila
ccd673bf05
🧰 fix: Keep File-Backed Agent Capabilities and Their Saved Tools in Agreement (#15418)
An agent with knowledge files showed File Search as selected in the builder while
the save omitted `file_search` from its tools. At runtime the tool was gone, the row
still looked selected so re-adding it changed nothing, and the next save re-confirmed
the broken state. Recovery needed a MongoDB write.

The builder's model for file-backed capabilities is stated in ToolsSection: they stay
selected while they hold files, and removal is routed to the file manager rather than
the capability flag — mirroring the file-only `context` built-in. Three places
implemented that model separately and drifted:

- display resolved it from the flag OR the file count,
- persistence resolved it from the flag alone,
- the removal guard lived inline in the tools list and was missing entirely from the
  marketplace dialog.

That last gap is how the bad state was reached. `ToolsMarketplaceDialog` toggled a
selected File Search straight to `file_search: false` with no file check, producing
flag-off-with-files — a state display cannot show and persistence silently drops.

`items/capabilities.ts` now owns the capability table and the one rule, and display,
removal and persistence all derive from it:

- persistence resolves the same way display does, so a file-holding capability keeps
  its tool (this also heals already-broken agents on their next save),
- both toggle paths route removal of a file-holding built-in to the file dialog,
- the removal guard takes file counts rather than the agent, so a component can pass
  the hydrated entries it already watches while submit reads the form — the rule is
  shared, the source of the count need not be.

Submission also stops mutating `data.tools` in place and dedupes the result.

Supersedes the persistence half of #15380, which fixed the save but left the
marketplace path able to recreate the state and kept the rule in a third copy.

Fixes #15368
2026-08-31 09:47:32 -04:00
alexsr1
7b0b145159
🪁 fix: Keep Tool Flyouts in View (#15404)
* fix: keep tools menu submenus within the viewport

* fix: let the viewport cap override the submenu min-width
2026-08-31 09:42:52 -04:00
Danny Avila
26887bc5a9
🛗 chore: Elevate @librechat/agents to 3.7.11 and multer to 2.3.0 (#15409)
* 📦 chore: bump `@librechat/agents` to v3.7.11

* chore: bump multer to latest
2026-08-31 08:51:30 -04:00
Danny Avila
52f7251ef7
📦 chore: Bump Packages from LibreChat SCA Audit (#15410) 2026-08-31 08:51:17 -04:00
Danny Avila
413906abc3
🖍️ fix: Honor Agent Image Detail Settings (#15407)
* 🖼️ fix: Apply an Agent's Configured Image Detail

The Agent Builder exposes an Image Detail slider and saves it to
`model_parameters.imageDetail`, but nothing ever read it back: `encodeAndFormat`
resolves the level from `req.body.imageDetail`, which the agents route never
populates, so every image was encoded as `detail: "auto"`. Providers that reject
`auto` — MiniMax's OpenAI-compatible API among them — fail every image upload
through an agent, and the setting that would fix it has no effect.

The root cause is a classification error rather than a missing argument.
`extractLibreChatParams` splits LibreChat's own settings out of an agent's model
parameters, and its key set is derived from the `librechat` group in
`parameterSettings`. `imageDetail` sat in `baseDefinitions` instead, so it was
neither extracted for LibreChat's use nor stripped from the options handed to the
provider.

It is now grouped with `resendFiles` and the other LibreChat-level parameters,
which gives it the same path those already take: extracted during agent
initialization, carried on the initialized agent, passed to the client, and read
by `encodeAndFormat`. `AgentClient.getSaveOptions` already expected
`options.imageDetail` — nothing had ever populated it.

The request body still wins where it is set, so the classic endpoints keep
resolving detail from conversation settings exactly as before.

Fixes #15359

* 🔤 fix: Type Image Detail as Its Enum, Not a Bare String

`TConversation.imageDetail` is the `ImageDetail` enum, so the extractor's return
type and the initialized agent's field are now declared as that enum rather than
widened to `string`, and the tests pass enum members instead of raw literals.

Caught by CI's type check, which I had run before adding the tests and not after —
jest compiles the specs through babel, so the suite stayed green over a type error.
2026-08-31 08:34:05 -04:00
Danny Avila
e9e77a4b33
🧜‍♀️ chore: Bump mermaid to v11.17.2 and dompurify to v3.4.14 (#15405)
Bumps mermaid 11.16.1 -> 11.17.2, and raises the dompurify override from
3.4.12 to 3.4.14 to clear GHSA-55q2-fjhq-7xh7 (XSS: IN_PLACE hook removal
leaves a detached subtree executable).

The dompurify pin lives under the `monaco-editor` override because monaco
depends on an exact 3.4.8; the override lifts it to match what the rest of
the tree resolves for `^3.4.0`, so a single copy stays hoisted. Bumping it
to 3.4.14 (current latest) keeps that single-copy property — pinning 3.4.13
would have left `^3.4.0` on 3.4.14 and installed two copies.

The mermaid floor moves to ^11.17.2 rather than staying ^11.16.1 because
11.17.0/11.17.1 dropped the `edgePaths` class from rendered SVG, breaking
the flowchart, block and user-journey stylesheets. Restored in 11.17.2.

Lockfile delta is limited to mermaid 11.16.1 -> 11.17.2, @mermaid-js/parser
1.2.0 -> 1.2.1, dompurify 3.4.12 -> 3.4.14, plus new fastdom + strictdom.
katex is unaffected: mermaid raised its floor to ^0.16.47, but the global
`katex: ^0.16.21` override supersedes it, and mermaid only ever calls
`katex.renderToString()`, which 0.16.28 provides.
2026-08-31 08:01:11 -04:00
Danny Avila
044894e8bb
🎛️ fix: Settle New Turns at the Bottom, Steady Status Dots, and Elapsed Time in the Panel (#15394)
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: Settle New Turns at the Bottom, Steady Status Dots, and Elapsed Time in the Panel

* 🎛️ fix: Anchor Panel Timers, Order Turns by Trigger Time, Keep a Non-Color Status Cue
2026-08-30 23:11:00 -04:00
Danny Avila
14d4f2789d
🍃 fix: Send a Pure Inclusion Projection From the Legacy Receipt Sweep (#15390)
Some checks are pending
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: Send a Pure Inclusion Projection From the Legacy Receipt Sweep

The legacy actor-receipt sweep selected its candidates with
`.select('_id +agentEventActorReconciliations')`. `_id` alone does not make a
Mongoose projection inclusive and a `+` token only un-hides its field, so this
compiles to `{ _id: 1 }` plus a `: 0` exclusion for every other
`select: false` sibling — a mixed projection MongoDB tolerates via the `_id`
exception but Amazon DocumentDB rejects (`Projections cannot have a mix of
inclusion and exclusion`). The sweep failed on every 30-second maintenance
pass, receipts never expired, and the rejection took the whole Promise.all
down with it, so the sequenced lane reclamation never ran either.

- The candidate read uses the object form `{ _id: 1,
  agentEventActorReconciliations: 1 }` — a pure inclusion that still returns
  the hidden field and fetches nothing extra. A regression test spies on the
  driver call and asserts the compiled projection contains no exclusions.
- Every maintenance step now fails alone: a rejection is logged with its step
  name and counted as zero progress instead of aborting the pass, and lane
  reclamation runs regardless. Covered by a test rejecting one step and
  asserting reclamation still runs.
- The static guard gains a detector for the exact broken shape —
  `select('_id +field')` — with fixtures for both directions. The shape is
  narrow on purpose: probing real Mongoose shows a bare non-`_id` token makes
  the projection inclusive and turns `+` tokens into plain `field: 1`
  inclusions, so the repo's nine other `+`-selects are all legal and stay
  unflagged. The live audit suite gains a raw mixed-projection probe and
  drives the sweep itself as production shape number eight.

* 🍃 fix: Gate Lane Reclamation on Batch Recovery and Type the Projection Assertion

Review round 1, both findings real:

- Batch-receipt recovery is a prerequisite of lane reclamation, not an
  independent step: reclamation consumes the lane-cleanup markers
  (`fulfillLaneCleanupRequest` unsets the delivery-side marker, and
  `reclaimLaneIfInactive` clears `cleanupRequestedAt` when the lane is still
  retained), so running it against a half-recovered batch erases a request no
  later successful recovery can re-arm, retaining the lane permanently. The
  blanket isolation reintroduced exactly that; reclamation now waits for a
  batch-recovery pass that did not fail, while the genuinely independent steps
  keep failing alone. Covered by a test rejecting batch recovery and asserting
  reclamation is withheld.

- The captured-projection assertion drops its `Record<string, unknown>` cast
  and asserts deep equality with the exact expected pure-inclusion projection,
  which is both typed and strictly stronger.
2026-08-30 22:10:44 -04:00
Danny Avila
c93bd7ec9d
🪟 fix: Keep the Subagent Panel Steady and Honest Across Event Deliveries (#15387)
* 🪟 fix: Keep the Subagent Panel Steady and Honest Across Event Deliveries

* 🧮 fix: Budget the Activity Projection at the Database Seam and Scope Retained Views

* 🧮 fix: Charge Label Associations to the Budget and Scope History Flags

* 🧮 fix: Gate Mixed Activity Metadata and Keep the Budgeted Suffix Contiguous

* 🧵 fix: Render Only the Active Thread's Retained Turns
2026-08-30 21:58:44 -04:00
Danny Avila
c7f0a8f247
📬 feat: Move Agent Queued Turns Server-Side (#15370)
* feat: move agent queued turns server-side

* test: update queued turn composition coverage

* fix: harden queued-turn lifecycle ownership

* fix: centralize queued-turn lifecycle ownership

* fix: close queued-turn lifecycle gaps

* fix: unify queued turn lifecycle transitions

* fix: fence queued-turn deletion admission

* fix: preserve queued-turn lifecycle ownership

* fix: close queued-turn reconciliation races

* fix: reconcile quarantined queued-turn admissions

* fix: annotate admission evidence default

* fix: deepen queued turn lifecycle admission

* fix: fence queued turn provider admission
2026-08-30 21:53:58 -04:00
Danny Avila
4de0a9341b
✍️ chore: Clarify That Interrupting a Steer Does Not Interrupt Reasoning (#15386)
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
The interrupting receipt's hover implied the interrupt cuts in
immediately, but the seal waits for reasoning to finish. Say so, and
point at stopping the response as the way to interrupt reasoning.
2026-08-30 20:19:51 -04:00
Danny Avila
56d54a9416
📦 chore: bump vite to v8.2.2 (#15384)
Some checks are pending
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
Refreshes the lockfile to the version the existing `^8.0.16` range already
resolves to, so `npm install` and `npm ci` stop disagreeing about which Vite
builds the client. Carries rolldown 1.0.3 -> 1.2.6 with it.
2026-08-30 20:03:41 -04:00
Danny Avila
5fdc4f8656
🔒 chore: bump google.golang.org/grpc to v1.82.2 (GHSA-hrxh-6v49-42gf) (#15383)
Resolves the high-severity grpc-go advisory (xDS RBAC authorization
bypass, HTTP/2 rapid-reset mitigation bypass, xDS RBAC panic) flagged
against `otel/langfuse-fanout`. grpc is a transitive dependency there,
linked only because the OTLP collector protobufs carry generated gRPC
service stubs; the service itself speaks OTLP over HTTP and never
starts a gRPC server or uses xDS.

Carries `protobuf` to v1.36.11 and the `genproto` stamps forward, as
required by grpc v1.82.x. No source changes.
2026-08-30 20:00:08 -04:00
Danny Avila
9472d51dac
🧪 fix: Align Detached Activity E2E With the Removed Running Chip (#15382)
* 🧪 fix: Align Detached Activity E2E With the Removed Running Chip

* 🧪 fix: Drop Narrating Comment From Detached Activity Spec
2026-08-30 19:53:59 -04:00
Danny Avila
1e4cae07c7
🧠 feat: Retain Subagent Reasoning Like Main Chat and Drop the Running Status Chip (#15379)
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
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
2026-08-30 19:33:31 -04:00
Danny Avila
fcae1025c0
🧳 feat: Register Principal-Owned Code Environments (#15365)
* feat: add principal-owned code environments

* fix: address code environment CI coverage

* fix: expose principal code environments in endpoint config

* fix: reject missing code environment bodies

* fix: harden code environment lifecycle

* fix: revalidate principal code environments

* fix: synchronize code environment authorization

* fix: bind code environments to current principals

* fix: fail closed on code ACL cache errors

* fix: prevent code environment override shadowing

* fix: preserve principal environment defaults

* fix: suppress revoked code environment aliases

* fix: fail closed on environment augmentation

* fix: narrow code environment defaults

* fix: isolate code environment fallback

* style: sort code config imports
2026-08-30 19:33:19 -04:00
Danny Avila
0389a92452
📦 chore: bump @librechat/agents to v3.7.10 (#15378) 2026-08-30 19:28:22 -04:00
Danny Avila
cd46d50810
🕯️ style: Settle the Applied Steer Receipt Once the Response Is Done (#15377)
The applied checkmarks kept the amber steering identity forever, so a
conversation full of historical steers stayed lit long after anyone
cared whether they landed. The receipt now keeps amber only while the
owning response is still generating (isSubmitting, the window where it
is the live thing at the end) and settles to the timestamp gray with a
soft color transition. Reload, share, and search render settled.
2026-08-30 19:18:52 -04:00
Danny Avila
9ac801ee45
✅ feat: Delivery Receipt Checkmarks for Steered Messages (#15376)
* ✅ feat: Delivery Receipt Checkmarks for Steered Messages

Users regularly read steering and interrupt as broken because nothing
visibly reacts: the in-flight bubble's only applied signal is its own
disappearance, an armed interrupt is a silent icon swap, and the inline
part's explanation hides behind a hover-only "?".

Every phase now shows an explicit, immediate receipt driven by events
the protocol already emits (no server changes):

- Shared SteerReceipt (Chat/Steering/Receipt.tsx): Sending… →
  Delivered ✓ (202 ACK) → Interrupting (amber ✓ + pulsing dot on
  preempt arm) → Applied ✓✓ on the inline part, with hover cards
  explaining each state (the info text doubles as the accessible name).
- The applied ✓✓ replaces SteerPart's hover-gated "?" in the same
  32px meta row and stays visible on reload, share, and search views.
- A one-shot draw-in plays only at the live chip→inline hand-off:
  resolveSteerChip stamps liveAppliedSteerIds in the same batch as the
  chip removal and SteerPart consumes the id on mount.
- An unacknowledged interrupt shows the amber label and pulse without a
  check: a check only ever asserts what the server confirmed. Failed and
  delivery-uncertain steers keep their recovery rows, checkless.
- Queued rows gain one caption for the group ("Sends when this
  response finishes"), shown only while the run is active.

* 🤝 fix: Address Codex Round 1 on Steer Receipts

- Extend InfoHoverCard with an optional custom trigger (children) and
  compose the receipt through it instead of duplicating the hover-card
  state, portal, and focus handling in the feature.
- Replace the feature-specific style.css keyframe with the existing
  tailwindcss-animate utilities (animate-in fade-in-0 zoom-in-50
  duration-theme-normal, motion-reduce:animate-none), matching the
  house pattern in Thinking.tsx.
- Scope the live-applied subscription per steer id via a membership
  selectorFamily so stamping/consuming one receipt no longer re-renders
  every mounted historical SteerPart.
- Gate the live stamp on a genuinely first application: a replayed
  on_steer_applied whose ids are already in the durable applied set no
  longer re-arms an animation its part already consumed.
- Track a row-local arming flag so escalating a delivered steer flips
  its receipt to the unconfirmed Interrupting state on the click, with
  the check appearing only on the confirmed durable arm (and reverting
  to Delivered on a lost race).

* 🤝 fix: Address Codex Round 2 on Steer Receipts

- Stamp the live-applied id only when the inline part is actually
  committed: resolveSteerChip now returns the first-application ids and
  applySteerToMessages threads them through its bounded retries,
  stamping in the same batch as the setMessages commit. An abandoned
  placement or a replayed (referentially stable) event can no longer
  strand an id that would animate a historical part on a later visit.
- Move the queued caption outside the role=list container so the list
  owns only listitem rows, with a structure test.

* 🤝 fix: Stamp Only the Rendered Steer Id as Live-Applied

The applied event settles both the server id and its client alias, but
the inline part renders (and therefore consumes) only part.steerId, so
stamping both left the alias resident in the live set as dead weight.
Stamp exactly the rendered id.

* 🤝 fix: Address Codex Round 4 on Steer Receipts

- Capture the live draw-in per steer IDENTITY, not per mount: a content
  slot overwritten with a different steer (applySteerPart permits index
  replacement) re-derives its flag and every identity consumes its id,
  so nothing lingers to animate a historical part later.
- Treat a relabelled chip (preempt past sending) as confirmation even
  while this row's arm request is pending: the SSE steer_updated can
  deliver the durable arm before the HTTP response, and the check must
  not wait out the round-trip timeout.
- Announce receipt transitions (Sending to Delivered, Interrupting)
  through the row's existing polite live region; mount state is not
  replayed.
2026-08-30 18:33:39 -04:00
Danny Avila
9684b2478f
🍃 fix: Restore Amazon DocumentDB Compatibility Across Trigger and Subagent Writes (#15375)
* 🍃 fix: Restore Amazon DocumentDB Compatibility Across Trigger and Subagent Writes

The README advertises Amazon DocumentDB 5.0+ support, but six sites added
between 2026-07-29 and 2026-08-30 reintroduced constructs the engine rejects,
and the transaction probe silently reported a false negative. All seven are
adjudicated against a live DocumentDB 5.0.0 cluster, not against AWS's
omission-based documentation.

Rejected by every engine version, now rewritten with supported operators:

- `claimNextAgentTriggerDelivery` used a pipeline-form update. It reads the
  globally oldest candidate under the same sort, then claims that exact `_id`
  and lifecycle state with a compare-and-swap. Ownership is still granted only
  by the fenced write. An empty queue costs the one read it costs today; a
  successful claim costs one additional round trip.
- `renewAgentTriggerDeliveryProducerLease` used a `$cond` to keep the later
  deadline. `$max` is the exact classic-operator form and evaluates no
  expression.
- `claimBackgroundToolResults` and `releaseBackgroundToolResultClaims` rewrote
  the entire `content` array through `$map`. The filtered positional operator
  addresses the matching parts directly, so each write touches only those parts
  and still needs no read-modify-write.
- The bounded subagent thread-view projections used `$$REMOVE` to omit keys.
  They now emit `null` and are pruned back to absent inside the pass that
  already materializes each record, preserving the absent-vs-present contract.
- The selected-task read used `$facet`. It is now two concurrent reads, which
  also removes the 16 MiB single-document risk the surrounding code already
  avoids for the same reason.

`supportsTransactions` probed a canary collection that does not exist, and
DocumentDB rejects a transaction touching a non-existent collection. The probe
returned `false` on an engine that fully supports transactions, so every caller
silently took the non-transactional path. It now materializes the canary first,
once per process.

Adds `documentdb.spec.ts`, a static guard over the whole class, because no
runtime test can catch it: every suite runs `mongodb-memory-server`, which is
real MongoDB and accepts all of these constructs. Adds
`misc/documentdb/audit.documentdb.spec.ts` to re-adjudicate against a real
cluster, and records the live verdicts and connection requirements in the
compatibility assessment.

* 🍃 fix: Address Review — CAS Exhaustion, AST Guard, Single-Snapshot Selected Read

Addresses all three review findings, and the strengthened guard immediately
found a seventh incompatible site the original character-level scan missed.

- Exhausting the claim's compare-and-swap budget now throws instead of
  returning null. Every lost round read a claimable row, so the queue is not
  empty; null is the engine's confirmed-empty signal and advances its idle
  backoff, which would leave queued work waiting. A throw lands on the engine's
  retryable claim-failure path, which holds the polling cadence. Covered by a
  new test asserting the bounded budget and that the loser owns nothing.

- The static guard now walks the TypeScript AST: it catches pipelines bound to
  a variable first, cast with `as`, chosen by a conditional, or carried in a
  `bulkWrite` operation's `update` property (including shorthand). Every shape
  is proven against fixtures in the suite itself, so the detectors can never
  silently go inert. The stronger scan flagged `updateToolCallResult`, whose
  pipeline lived in a `stages` variable: the backgrounded tool-call settle path
  was still broken on DocumentDB. Rewritten with the filtered positional
  operator — subfield sets preserve an existing `resultClaim` without per-part
  branching, an explicit `$unset` keeps the old whole-object replacement's
  wakeup-disarm semantics, and the attachments merge becomes `$pull` plus
  `$push` (two atomic writes that converge under the retry the claim fencing
  already provides). New tests lock the disarm and supplied-claim semantics,
  and the live audit suite now drives this method too.

- The selected-task read is a single aggregation again: each selected row
  yields both its bounded message fields and its source payload from the same
  document version, restoring the `$facet` snapshot guarantee the concurrent
  split lost, and returning the endpoint to three reads.

* 🍃 fix: Close Settle Atomicity Windows and Spread Claim Contention

Self-review pass (eight independent review angles over the branch diff).

Correctness:

- The attachments merge is a single fenced write again: read the array, merge
  with the exact semantics the old `$filter`/`$concatArrays` had, and write it
  back with the settle patch in one update guarded on the array being unchanged
  — the same compare-and-swap this file already uses for control receipts. The
  `$pull`-then-`$push` split could strip attachments on a failure between the
  writes (the throw path retires the delivery, so nothing healed it) and
  duplicated them under concurrent re-application.
- A supplied `resultClaim` is stamped BEFORE the settle write instead of after
  it. The claim-last order left a terminal, claimable, unclaimed part visible
  to the ~1s wakeup poll, which could acquire and deliver a result a manual
  poll had already delivered; claim-first leaves only inert intermediate states
  (a claim on a part that is not yet terminal admits no consumer). The stamp's
  guard is `resultClaim: null`, restoring the old `$ifNull` treatment of a
  stored null.
- `releaseBackgroundToolResultClaims` filters on `content: { $type: 'array' }`:
  the filtered positional operator errors on a row without the array where the
  old pipeline no-op'd, and that throw surfaced as retryable release failures
  that left claims stamped forever.
- The claim loop reads a BATCH of candidates per round, so the engine's
  parallel claimers spread across the batch instead of convoying on one
  head-of-queue row; exhaustion now throws the typed
  `AgentTriggerClaimContentionError`. The dead-capability adoption fence also
  pins `availableAt`, which the write copies.
- The transaction probe probes first and creates the canary only after a
  failure — plain MongoDB never pays the DDL or gains the collection — logs a
  failed create instead of swallowing it, and concurrent first probes share one
  in-flight promise.

Structure and guard:

- One `partScope(prefix)` builder yields the `$elemMatch` document filter and
  the arrayFilters element filter from a single predicate, deleting the
  aggregation `$expr` twin, and the agent-ownership rule shared by settling and
  claiming lives in one `agentOwnershipFilter` helper.
- The static guard scans all three backend workspaces (`packages/data-schemas`,
  `packages/api`, `api`), parses each file once, and shares the tree between
  both detectors.
- The audit suite asserts each probe's returned verdict, the compat suite's run
  recipe carries the connection parameters the audit header documents, and the
  `$facet` pipeline assertion covers every captured aggregation in one loop.

* 🍃 fix: Keep Attachment Contention Retryable and the Selected Snapshot Authoritative

Second codex round, both findings real:

- Exhausting the attachment-merge fence now returns an unmatched result with a
  warn log instead of throwing. The settle retry loop treats unmatched as
  retry-then-heal-later and degrades to false; a throw escaped it onto
  ambiguous-failure handling that can retire the completion, losing a healthy
  completed tool result to mere contention. Locked by a test asserting the
  bounded rounds resolve to `{ matched: false }` rather than a rejection.

- When the selected task's rows also appear in the normal page window, the
  dedup pass kept the page copy — read by an independent concurrent aggregate —
  and discarded the single-snapshot version, letting a page row claim a
  transcript that `selectedSources` does not carry. Duplicates are now replaced
  in place with the snapshot version (page order kept), so the selected
  message/source pair stays per-document coherent end to end.

* 🍃 fix: Treat a Stored-Null Claim Stamp as Unclaimed in the Claim Fence

Third codex round. The subfield-preserving settle write keeps a persisted
`resultClaim: null` that the old whole-object rewrite dropped, and the claim
path's outer `$elemMatch` fence still required the field to be absent via
`$exists: false`. The in-memory claimable scan, the claim arrayFilters, and the
settle stamp all already treat a null stamp as unclaimed, so a part carrying
one became terminal but permanently unclaimable: every manual poll and wakeup
read `ready` in memory, missed the fence, and returned `not_ready` forever.

The fence now matches missing-or-null like every other layer. The regression
test persists an actual null stamp — asserted present before claiming, so the
test cannot pass vacuously — and claims it.
2026-08-30 18:33:25 -04:00
Danny Avila
a9ccac8656
🧲 feat: Enable Secure Attached Environment Pairing (#15355)
* feat: add secure code environment pairing

* fix: satisfy code environment type checks

* fix: secure code environment administration

* fix: isolate code pairing control plane

* fix: validate code pairing control responses

* fix: secure code pairing transport

* fix: validate code pairing wire format

* fix: harden pairing secret lookup
2026-08-30 17:12:20 -04:00
Danny Avila
7533d138fa
🧬 perf: Evolve Compaction Guidance on Warm Turns (#15371)
* perf: evolve compaction guidance on warm turns

* style: sort compaction adapter imports
2026-08-30 17:11:20 -04:00
Danny Avila
9dcef360e2
🧳 fix: Carry Stateful Environments Through Runtime Config (#15374) 2026-08-30 17:08:24 -04:00
Danny Avila
29b3e2ef3e
📜 fix: Resolve MCP Server Instructions for Startup-Deferred Servers (#15361)
* fix: Fetch MCP Instructions from the First Live Connection

Startup inspection intentionally defers servers that need per-user or runtime context, including OAuth/OBO, custom variables, user API keys, runtime placeholders, and startup-disabled servers. An enabled serverInstructions declaration therefore never resolves to text during inspection, even though the first live connection already has the instructions from the initialize response.

Backfill resolvedInstructions from that connection through an identity-preserving YAML cache patch. Preserve updatedAt so live connections do not become stale, and globally invalidate the tenant-scoped read-through caches because YAML entries are shared across tenants. Literal instruction strings continue to win.

Scope remains YAML-tier servers. Config-overlay servers are keyed by config hash, and DB-backed user servers need a separate identity-preserving write through mongoose timestamps and credential sanitization.

* fix: Surface per-identity MCP instruction divergence

`resolvedInstructions` is a single field on a config shared by every user
of the server, and for a startup-deferred server the text now comes from
one user's authenticated connection. That is exact for a server
advertising one static block, but a server that tailors instructions per
identity cannot be represented by it.

Rather than let the stored copy churn per connection — each write
invalidates the read-through cache globally, and the model context would
vary by whoever connected last — keep the first text and log the
divergence, so the assumption is diagnosable instead of silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* perf: Skip MCP instruction backfill for non-YAML tiers

`setResolvedInstructions` writes only the YAML tier, so a config-overlay,
user, or plugin server reached it, spent a cache round-trip — a network
hop under Redis — and was refused. That repeated on every connection
creation, because the refusal leaves `resolvedInstructions` unset and
nothing memoizes the outcome.

Gate on the existing `isUserSourced`/`isPluginSourced` predicates plus an
explicit `config` check. An unset source still proceeds: it predates
per-tier stamping and the registry resolves it by name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* test: Pin the MCP instruction context read path

Every existing assertion read back through `getServerConfig`, but
`MCPManager.getInstructions` resolves instructions from
`getAllServerConfigs`, which is served by a different read-through cache.
A backfill that invalidated only the per-server cache would pass the
suite and still leave the reported bug unfixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: Refuse MCP instruction backfill from mismatched configs

Self-review findings on the backfill, both in the shared-copy write:

A config-tier override shadowing a YAML base keeps the base's 'yaml'
source tag (`overlaySource`), so the connection manager's tier guard
cannot see it, and instructions fetched from a tenant's overridden
endpoint would be patched into the shared global YAML entry — reaching
every other tenant's model context and persisting after the override is
removed. `setResolvedInstructions` now takes the config the delivering
connection was created from and compares it field-wise against the
stored entry over ADMIN_CONFIGURABLE_FIELDS, refusing on mismatch.
Field-wise rather than whole-object, since inspector-derived fields
legitimately differ.

The skip condition also only refused *identical* text, so a connection
built from a stale read-through snapshot (resolvedInstructions still
unset) could overwrite already-stored different text — violating the
documented first-write-wins invariant and re-triggering global cache
invalidation per divergence. The condition is now `!= null`.

Documented the aggregate-key cross-instance write race alongside its
existing tolerance for `reinspectServer`: the backfill patch fires at
most once per server per registry lifetime, and the atomic-write
upgrade (hash fields or Lua CAS) is the follow-up that closes the
race for every writer at once.

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

* fix: Scope deferred MCP instructions safely

* fix: Narrow optional Keyv namespace in Redis store detection

Keyv types `namespace` as `string | undefined`, so passing it straight
into `FORCED_IN_MEMORY_CACHE_NAMESPACES?.includes(...)` fails
`tsc --noEmit` in both cache classes — tsdown builds do not catch it,
but the TypeScript type checks CI job runs tsc and would. An unset
namespace (never the case after construction) now reads as not
Redis-backed, which falls back to the guarded non-Lua path.

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

* fix: Harden the shared-instruction gate and CAS the patch

Codex round two, both verified before fixing:

A configured `oauth` block slips the backfill gate whenever
`requiresOAuth` is not literally true. The inspector stamps
`requiresOAuth = false` on every `startup: false` server without
consulting `oauth`, so the stamped population connects bare and fetches
anonymously — but the gate's safety rested entirely on that stamp: a
config reaching the manager unstamped gets OAuth machinery armed
(`isOAuthServer` treats `oauth != null` as OAuth) while
`requiresUserScopedConnection` waves it through. The gate now rejects
`oauth`/`oauth_headers` outright; genuinely static servers carry
neither.

The registry validates config identity against a snapshot that can lag
by the cache TTL, while the Lua patch checked only that
`resolvedInstructions` was unset — so a replica could validate against
an old entry, another replica replace it, and the patch land
instructions on the replacement. `patch` now takes the validated
entry's `updatedAt` and both Lua scripts (and the in-memory and
fallback paths) refuse when the stored entry no longer matches:
identity validation and the write are one compare-and-set.

Both guards verified red-without-fix; suite 36/36.

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

* fix: Loosen apiKey on the scoping config and sort imports

CI caught two things local gates filtered past:

`UserScopedConnectionConfig` gained `apiKey` on the strict Pick side,
but raw (pre-inspection) configs carry an optional `apiKey.source` —
exactly what the type's loosened intersection exists for — so
`agents/initialize.ts` stopped compiling. The gate only reads
`apiKey?.source`, so the loosened shape is sufficient and the
TypeScript type checks job goes green again.

The `canBackfillSharedServerInstructions` import landed unsorted in
UserConnectionManager.ts, failing the changed-file import-sort gate.

Verified with a full `tsc --noEmit` error-list diff against clean dev
(zero branch-only errors) rather than per-directory counts, which is
how the initialize.ts error slipped local verification.

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

* fix: Make MCP aggregate writes atomic

* test: Fix Redis aggregate spy assertion

* fix: Reject placeholder-bearing admin keys from shared backfill

Codex round three P1, verified end-to-end before fixing: processMCPEnv
injects an admin `apiKey.key` into the request headers (env.ts:448)
BEFORE header values get per-user placeholder resolution (env.ts:478),
so a key like `{{LIBRECHAT_OPENID_ACCESS_TOKEN}}` makes the connection
identity-scoped — while `placeholderBearingFields` never inspects
`apiKey.key` and the gate rejected only `source: 'user'`. Instructions
fetched under one user's identity could then be stored for everyone.

The gate now scans the admin key value with the same runtime-placeholder
predicate. Kept narrow deliberately: widening
`placeholderBearingFields` itself would change
`requiresUserScopedConnection` for every caller — connection pooling
included — which is its own decision.

Static admin keys still backfill (positive control test); both new
refusal tests verified red without the gate change. Suite 39/39.

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

* refactor: Drop gate term covered by placeholder-bearing apiKey

eb117d1b4 added `apiKey.key` to `placeholderBearingFields`, so
`requiresUserScopedConnection` now rejects placeholder-bearing admin
keys for every caller — connection pooling included — and the explicit
scan in `canBackfillSharedServerInstructions` from the rebased
32d598692 became a duplicate of that broader check. The refusal tests
stay green through the shared path alone, which also confirms the
broader mechanism covers the round-three finding.

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

* fix: Preserve empty arrays in Redis aggregate mutations

* fix: Scope env-expanded MCP placeholders

* test: Harden Redis empty-array preservation

* style: Fix Redis cache static checks

* test: Narrow Redis empty-array fixtures

---------

Co-authored-by: Simon Guldager <sg@nobly.dk>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-30 16:17:26 -04:00
Danny Avila
91cfd04f22
🏕️ fix: Restore Code Environments From Runtime Config (#15373) 2026-08-30 16:15:47 -04:00
Danny Avila
30124f21b2
🎻 refactor: Orchestrate Agent Runs Through a Request-Free Host (#15366)
* refactor: decouple agent initialization from HTTP

* refactor: centralize remote agent execution lifecycle

* fix: preserve pre-settlement error rendering

* fix: preserve request-backed tool loading

* style: sort agent execution imports

* fix: adapt public agent tool loaders
2026-08-30 15:38:57 -04:00
Danny Avila
c1cb591d49
🗿 feat: Add Attached Stateful Code Environments (#15352)
* feat: add attached stateful code environments

* fix: include code environment in lazy agent type

* fix: harden stateful environment routing

* fix: complete code environment route isolation

* fix: declare code environment map type

* fix: preserve configured code execution routes

* fix: harden stateful environment updates

* test: assert route-scoped sandbox readiness

* fix: harden stateful environment lifecycle

* fix: isolate migrated code sessions

* test: assert route-qualified code sessions
2026-08-30 15:38:45 -04:00
Danny Avila
dc77b78d3e
🔇 refactor: Quiet Framework Logging in Unit Tests (#15363)
* 🔇 refactor: Quiet Framework Logging in Unit Tests

Unit test output was overwhelmingly framework log lines rather than test
results. A `packages/api` run printed 9,076 lines for 434 suites; 1,596 of
them were the identical `at Console.log (winston/transports/console.js:87)`
frame that Jest staples onto every winston write.

Three causes:

- Each winston logger built its Console transport with a hard-coded
  `level: 'info'`. Winston resolves a transport's explicit level ahead of
  its parent's, so the logger's own `NODE_ENV`-derived `warn` never applied
  — in tests or in production. `CONSOLE_LOG_LEVEL` now drives it, defaulting
  to the previous `info` so deployments are unaffected. `meiliLogger` had
  the same hard-coded level and is routed through the same helper.
- `LOG_TO_FILE` defaults on, so every Jest worker opened DailyRotateFile
  transports. A run left `packages/api/logs/` holding a 107KB error log, a
  gzipped rotation, audit JSON, and — from fake-timer suites — files dated
  1969 and 2023.
- Client suites carried leftover debug `console.log` in shipped source
  (`Test mode. Skipping silent refresh.` alone accounted for 109 blocks)
  plus three test-harness defects whose React error dumps buried everything
  else: a markdown suite rendering Mermaid with no Router, a `~/Providers`
  mock missing `useSearchContext` (the component's own catch swallowed the
  throw, so the sources path was dead while the suite passed), and
  `getBoundingClientRect` stubs without `left`, which made a computed
  `right` NaN.

A shared `config/jest.setup.logging.cjs` sets the two env vars for both
backend workspaces. It sets env only and requires nothing: eagerly requiring
the logger froze `CREDS_KEY` into `encryptV3` before specs could set it and
broke five suites. `TEST_VERBOSE_LOGS=true` restores the logs for debugging.

Output per full run:

  packages/api           9,076 -> 770 lines   (winston lines 1,596 -> 0)
  packages/data-schemas  2,521 -> 946 lines   (winston lines   290 -> 0)
  client                10,384 -> 3,153 lines (console blocks  285 -> 74)

Test counts are unchanged: client 476 suites / 5,827 tests green, and the
Ariakit `act(...)` warnings the selector suites emitted are gone rather than
suppressed.

* 🔧 fix: Settle Shutdown Timers Before Counting Them

`destroy()` finishes cancelling its fenced retirement timers on the tick
after it resolves. The count only returned to the baseline because a console
write from the logger happened to yield first — silencing that logging left
four timers still pending at the assertion, deterministically failing the
suite in CI's sharded, coverage-enabled run.

Advance the fake clock by zero before counting, so the assertion proves the
timers were cancelled rather than that something incidental yielded.

* 🛡️ fix: Validate CONSOLE_LOG_LEVEL Instead of Trusting It

Winston resolves a level name against the logger's level map, so a name
outside it resolves to `undefined` and the transport drops every message.
Passing `CONSOLE_LOG_LEVEL` straight through meant a typo (`warning`) or a
stray space silently muted all console output — a misconfiguration that reads
as a dead deployment.

Validate against the level set, normalize case and whitespace, and fall back
to the default with a warning on the way past, since the logger cannot report
its own misconfiguration.

The level map itself was duplicated verbatim in both loggers; it now lives in
`utils` beside the validation that depends on it.

* 🎚️ fix: Let CONSOLE_LOG_LEVEL Outrank DEBUG_CONSOLE

`DEBUG_CONSOLE=true` forced the console transport to `debug`, so on those
deployments every non-`silent` value of `CONSOLE_LOG_LEVEL` was discarded —
`CONSOLE_LOG_LEVEL=error` still emitted debug and info, defeating the control
it advertises.

`DEBUG_CONSOLE` now only moves the *default* level and keeps choosing the debug
format, so an explicit level stays in charge of verbosity. With no explicit
level the resolved default is still `debug`, unchanged for existing setups.

Covers the wiring with a spec that re-imports the logger per environment,
rather than only testing the resolver in isolation.
2026-08-30 15:26:30 -04:00
Danny Avila
b0a559876f
🧩 feat: Collapsible Wake-Up Task Cards and Subagent UI Consistency (#15364)
* 🧩 feat: Collapsible Wake-Up Task Cards and Subagent UI Consistency

* 🧩 fix: Codex Round 1 — Shared Composer Surface, Pinned Event Tasks, Durable Wake-Up Links

* 🧩 fix: Codex Round 2 — Pin Requested Event Tasks, Share Gating, Focus Return

* 🧩 fix: Codex Round 3 — Promote Composer Surface to @librechat/client Semantic Primitive
2026-08-30 13:53:16 -04:00
Danny Avila
cd3768ed1f
🍵 feat: Continue Late Steers in Warm Agent Runs (#15357)
* feat: continue late steers in warm agent runs

* chore: bump agents sdk to v3.7.9

* fix: guard terminal steer admission
2026-08-30 11:54:17 -04:00