Commit graph

5385 commits

Author SHA1 Message Date
Danny Avila
30cd1c2f2e
🔇 fix: Stop logging an absent create_file target as an error (#15591)
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 / publish (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
`create_file` reads its target before writing so it can tell a create from
an overwrite, so `readSandboxFile` missing the path is the ordinary case for
a create — yet it was reported at error level, through `logAxiosError`, which
rendered the sandbox's own `cat: …: No such file or directory` as "An error
occurred while setting up the request" plus a stack. Every ordinary file
creation emitted an error-level entry claiming a transport fault about a file
that was written half a second later, and an operator reading it concluded the
file had been lost.

Split the two failures `readSandboxFile` was conflating. A request that never
completed still goes to `logAxiosError`; a request the sandbox answered with
stderr is now classified on that stderr — an absent path logs at debug, and
anything else (permission, a broken interpreter) keeps error level with the
sandbox's own wording. The thrown error is unchanged, so the authoring flow's
create/overwrite decision still reads it the same way.

The classifier moves to `packages/api/src/files/code/errors.ts` and is now
shared: `handlers.ts` had two near-duplicate copies of it, the narrower of
which documented itself as narrower than the broader one without either
deriving from the other.

Also carry the cause on `[preflightCodeOutputBatch] … could not be inspected`.
That warning follows each `create_file` in the same request, and the batch
discarded the error entirely — so a Code API that refused the download, one
routed to the wrong execution profile, and a file too large to inspect all
arrived as the same sentence, and neither this warning nor the one above could
be told apart from the failures around them. It goes through
`getSafeErrorMetadata`, which is what keeps the upstream message and the
artifact's name — both of which can echo submitted content — out of the log.
2026-09-04 09:21:24 -04:00
Danny Avila
59ab01339d
🔎 fix: Report Measured Skill Catalog Truncation (#15590)
* 🔎 fix: Report Measured Skill Catalog Truncation

The catalog truncation warning assumed `SKILL_CATALOG_MAX_ENTRY_CHARS` was
the delivered length. It is only the first rung of the SDK's ladder:
`formatSkillCatalog` applies the per-entry cap, then truncates further
against its own context budget, and finally falls back to names-only.

Two consequences, both silent:

- A description under the 250-char cap can still be cut — no warning fired
  at all. Measured: 8 skills x 200 chars at a 20k context window reach the
  model at 85 chars each.
- When the ladder does engage past the cap, the warning overstated what
  landed — reporting 250 where the true figure was 60.

Measure the emitted catalog instead of assuming the cap held, and warn
separately when descriptions are dropped entirely.

`formatSkillCatalog` is no longer stubbed in the spec. The passthrough mock
never truncated, which is why no test could observe this class of defect.

* 🔁 fix: Measure Duplicate-Named Catalog Entries Positionally

Keying delivered lengths by skill name collapsed duplicates: the catalog
keeps every invocable duplicate as its own entry, so a name-keyed map held
only the last one's length and reported it for all of them.

A 400-char and a 100-char description sharing a name warned "truncated to
100 of 400" when the first entry actually reached the model at 250.

Match positionally instead. Entry order is the order passed to the
formatter, and the names-only fallback drops from the tail, so a skill the
catalog never reached keeps its 0 and reads as dropped.

* 🧵 fix: Detect Catalog Truncation By Containment, Not Line Parsing

Nothing strips newlines from a skill description — validateSkillDescription
checks type, length and trim-emptiness only — so a catalog entry is not one
physical line, and parsing it as one misreports three ways:

- a multiline description the catalog kept intact was measured from its
  first line alone and flagged as truncated
- a description starting with a newline measured 0 and read as dropped
- a continuation line imitating the next entry's marker desynchronized
  every measurement after it (a 400-char entry measured 8)

Entry boundaries are not recoverable from the emitted text, because a
description may contain the exact byte sequence that starts the next entry.
So stop parsing it. A description the catalog kept appears verbatim, and
containment detects truncation from any rung of the ladder without needing
boundaries at all — including the duplicate-name case, which the positional
pass already handled and which this preserves.

Drops the delivered-length figure from the warning. That number is what kept
being wrong, and the actionable signal for an author is that their
description is being cut, not by precisely how much.

* 🎯 fix: Scope Catalog Containment To The Matching Entry

Matching the description alone missed a drop whenever that text occurred
anywhere else in the catalog. The sharp case is the names-only fallback: a
one-word description like "research" collides with a skill *named* research,
so containment held and the warning was skipped for a description that had
been dropped entirely.

Match the whole `- name: description` rendering instead. That is still
containment — no entry parsing, so multiline descriptions, duplicate names
and continuation lines imitating the next marker all stay correct — but a
collision now has to reproduce the entry's full rendering rather than just
its text.

Skips empty descriptions, which render without the `: ` separator and have
nothing to truncate.

* 🔬 fix: Measure Catalog Truncation On A Length-Equivalent Probe

Matching the rendered entry still had a hole: when a multiline description is
cut immediately before text like "\n- next-skill: next description" and the
following entry is exactly that, the catalog holds the full rendering across
two entries and the check passes for a description that was truncated.

That is the fourth variant of one problem — the real catalog cannot be parsed
or matched, because descriptions are arbitrary text that can imitate any
structure the check relies on.

So measure somewhere unambiguous. Every decision in the truncation ladder
reads description `.length` and never description content, so formatting a
probe whose descriptions are same-length filler reproduces the real cuts
exactly, and the probe is parseable by construction: filler carries no
newline and no entry marker, and skill names are validated to
`^[a-z0-9][a-z0-9-]*$`.

This closes the class rather than the instance, and restores the delivered
length to the warning — verified against all six adversarial cases raised in
review, correct on 6/6.
2026-09-04 09:20:02 -04:00
Danny Avila
b76b02062a
🔬 test: Make the Paste Action's Hover-Gate Guard Actually Guard (#15589)
* 🔬 test: Make the Paste Action's Hover-Gate Guard Actually Guard

Review follow-up to #15588, and a correction: the guard it shipped could not
fail against the markup it exists to reject.

Two independent reasons, each enough on its own.

`toHaveTextContent` is a SUBSTRING match. #15588 adopted it over exact
`textContent` equality to shed whitespace brittleness and, in doing so, dropped
the only assertion in this test that was catching anything. The swap kept both
labels mounted and hid one in CSS, so the control read
"PlainMove back into message" — which a substring match accepts. Anchored now,
which keeps the whitespace normalisation that motivated the change.

The token sweep read `control.className`. The gate lived on the child spans and
never on the button, so it was scanning an element that never carried what it
was looking for. It sweeps the control's subtree now.

Verified by restoring the pre-#15586 component and re-running: the guard fails
against it, where both of the shipped forms passed.

* 🧪 test: State the Hover-Gate Invariant Instead of Listing Its Spellings

Codex P2 on the previous commit: the sweep named `group-hover`,
`group-focus-within` and `[@media(hover:none)]` — the three spellings this bug
happened to use — so the next spelling of the same idea walks past it.
`invisible hover:visible` on the control, or `hidden focus:block` on a
descendant, leaves the label absent at rest with the anchored text still
matching, because in jsdom the text is mounted either way.

Replaced with the rule the list was standing in for. A Tailwind token is
`variant:utility`; the utility is what follows the last colon, which holds for
bracketed variants too. Both halves of a gate are then rejectable on meaning
rather than on spelling: no utility may hide the label at rest, and no utility
under a hover or focus variant may reveal or hide it. Colour under those
variants stays permitted — that parity is asserted separately, and banning
`hover:` wholesale would contradict it.

Verified against all three spellings by patching the component: the historical
two-span swap, a direct gate on the control, and a direct gate on a descendant.
Each fails the guard.

* 🔭 test: Narrow the Hover-Gate Guard to What jsdom Can Decide

Codex found four ways past the previous commit's sweep, and two ways it
misfired. Bypasses: `text-transparent hover:text-text-secondary`, `opacity-[0]`,
Tailwind 3's `!hidden`, and a gate moved onto an ANCESTOR, which the traversal
never looked at. Misfires: `hover:inline-flex` is layout, not concealment, and
would be rejected; and a classed SVG descendant returns an `SVGAnimatedString`
from `.className`, so the sweep would throw before asserting anything.

Failing in both directions at once is the signal that the approach was wrong
rather than incomplete. Deciding whether a mounted node is visually hidden
needs a cascade, and jsdom loads no stylesheet — so every version of this was a
denylist wearing the language of an invariant, and a fourth would have invited
a fifth bypass.

Narrowed to the half that holds: the control's text is exactly the label, which
catches the swap that actually shipped, since it kept both labels mounted and
the control read "PlainMove back into message". The unobservable half is
written down as unobservable, with a pointer at where a real assertion belongs
— `toBeVisible()` in a browser, which `e2e/` has no composer-paste spec for.
2026-09-04 09:19:33 -04:00
Dustin Healy
ab0d74b492
✏️ feat: Add Agent Management Update Endpoint (#15566)
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 / publish (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
* feat: add Agent Management update endpoint

* fix: reject unsupported Agent update values

* test: simplify Agent update mocks
2026-09-04 00:16:57 -04:00
Danny Avila
9a43362663
🪤 chore: Scope Assistant Tool-Resource Lookups (#15579)
`DELETE /api/files` looked the assistant up by `id`, but the Assistant schema
stores it as `assistant_id`. `api/db/connect.js` sets `strictQuery`, under which
Mongoose drops filter keys absent from the schema — so the lookup degraded to
`findOne({})` and returned whichever assistant happened to be first in the
collection, regardless of owner.

The unlink path then read that stranger's `tool_resources` and intersected it
with the caller's requested files, so a file the caller named could be unlinked
on the strength of another user's assistant record. With an empty collection the
same lookup returned `null` and the unguarded `assistant.tool_resources` threw,
answering 400 instead of completing the request.

Filter on `assistant_id` and guard the dereference. Adds three route tests that
mirror the server's `strictQuery` setting, without which the harness cannot
reproduce either failure.
2026-09-04 00:14:44 -04:00
Danny Avila
bd83da552d
👓 fix: Match Pasted Text Focus Feedback (#15588)
Review follow-up. The label's colour shift answered the pointer only. The focus
ring already announces focus, so this is parity rather than a missing indicator
— but two input modes getting different feedback for the same state was a
difference with nothing behind it.

The hover-gate guard keeps asserting class tokens, brittle as that is. jsdom
loads no stylesheet, so a hover gate has no effect on the DOM under test and a
presence check cannot see one: the swap this replaced kept both labels mounted,
which is why the spec it replaced asserted both were in the document at once.
Recorded the reasoning where the next reader will meet it.
2026-09-04 00:07:08 -04:00
Danny Avila
1754297038
🛞 fix: Re-Exchange Rejected OBO Tokens (#15577)
* 🔑 fix: Re-Exchange Rejected OBO Tokens on MCP Auth Failure

An OBO MCP connection registered the non-OAuth fallback handler, so a 401/403
from the downstream server became `oauthFailed` without ever running another
token exchange. Every reconnect attempt replayed the credential the server had
just rejected.

Unlike interactive OAuth, an OBO bearer is minted from the user's live upstream
session, so a rejection is recoverable with no user-facing step — but only while
that session is reachable. `handleOboEvents` re-runs the exchange for a
connection whose creating request is still in flight, and retires the connection
otherwise, since `upstreamTokenProvider` closes over a finished request past that
point and the next borrower rebuilds against a live provider.

Three parts:

- `handleOboEvents` replaces the fallback for OBO connections: it re-exchanges
  once per attempt, updates the transport bearer, and emits `oauthHandled` so the
  connect retry proceeds.
- The refreshed token also replaces the runtime `authorization` header. Runtime
  headers outrank the transport's in `buildFetchInit`, so without this the stale
  bearer a previous tool call installed would keep winning on the retry.
- `resolveOboToken` gains `forceRefresh`, which bypasses the OBO token cache. A
  revoked or scope-invalidated token is still inside its cached lifetime, so a
  cached read would hand back the same rejected bearer.

A connection that cannot refresh now stops reconnecting rather than replaying the
rejected bearer through every backoff step and spending the circuit breaker's
cycle budget.

* 🔁 fix: Re-Exchange Rejected OBO Tokens on Tool Calls Too

Addresses both codex findings on c77be03.

A tool-call 401 never reached the connection-level handler: `oauthRequired` is
emitted only by `connectClient`, and `callTool`'s own recovery attaches solely
when `isOAuthServer(currentOptions) || connection.usesOAuth()` — both false for
an OBO-only config. It rethrew untouched, and the resolver kept serving the
rejected bearer from cache on every later call until it expired. This is the
dominant path for OBO traffic, so the cache-invalidation half of the previous
commit was not actually reachable in practice.

The per-call token resolution moves into `applyOboAuthorization(forceRefresh)`,
which the auth-error path re-runs with the cache bypassed before retrying once.
Recovery belongs here rather than at the connection level: the request is still
live, so `upstreamTokenProvider` can mint a replacement.

Also propagate the re-exchange failure. `connectClient` rejects its handling
promise with the mapped error and then rethrows the server's original 401, so a
diagnosis like an unrefreshable sign-in session was lost. `createConnection` now
surfaces it in place of the 401 — the same substitution the initial OBO
resolution already makes — and only for a genuine exchange failure, not for the
control-flow deferrals.

* 🔗 fix: Persist the Re-Exchanged OBO Token on the Connection

Addresses the round-2 codex finding on 09ccb34.

The tool-call recovery updated only the runtime request headers, leaving
`connection.oauthTokens` holding the rejected credential. That is not merely
untidy: a legacy SSE connection's event stream is served by
`eventSourceInit.fetch`, which bypasses `createFetchFunction` entirely and never
consults `getRequestHeaders()`. It sends the headers `constructTransport`
captured from `oauthTokens`, so the next transport rebuild re-baked the dead
bearer, 401'd, and — through the cached-connection handler added earlier in this
PR — retired a connection that had already recovered.

`applyOboAuthorization` now stores the resolved token via `setOAuthTokens`, the
same thing the connection-level refresh path does. Storing it on every
resolution rather than only on the retry also ends the pre-existing drift where
a connection's baked bearer aged out while tool calls used newer ones.
2026-09-04 00:06:44 -04:00
Danny Avila
78b320cbbc
🗜️ fix: Bound Conversation Page Size (#15576)
`GET /api/convos` read its page size as `parseInt(req.query.limit, 10) || 25`
and handed it to `getConvosByCursor` unclamped, where it becomes
`.limit(limit + 1)`. `?limit=-1` therefore issued `.limit(0)`, which MongoDB
reads as no limit at all, and the route returned the caller's entire
conversation list in one response; `?limit=999999999` forced the same unbounded
fetch. `attachSharedFlags` then amplified it with a second query whose `$in`
carried every returned id.

Every other list route already clamps, so the fix reuses that shape: the
projects handler's local `normalizeLimit` moves to `packages/api/src/utils/query`
and both routes share it, and `getConvosByCursor` bounds its own page size so
the invariant holds for any caller.
2026-09-03 23:52:36 -04:00
Danny Avila
374ed8df14
🧶 fix: Render Solo Agent Groups as Sequential Content (#15574)
* 🛤️ fix: Render a Single-Agent Group as Sequential Content

A multi-agent graph assigns a parallel group id to every starting node
(`MultiAgentGraph.computeParallelCapability`), so an ordinary agent that
merely has other agents loaded carries a `groupId` on its OWN run steps
and on every content part built from them. `groupId` is not evidence of
parallel execution.

The client treated `groupId != null` as parallel content in four places,
so an ordinary turn:

- rendered inside a `SiblingHeader` lane box, restating the message's own
  sender and adding a redundant branch control
- lost tool grouping, activity-label headers and client-synthesized phase
  folds, because lanes render raw parts
- widened the message row for columns that never arrive
- rendered the one group repeatedly when phase markers split it, since
  each segment runs its own `ParallelContentRenderer`

`~/utils/lanes.ts` adds `hasParallelLanes` / `MIN_PARALLEL_LANES`: a group
backed by one agent is not a comparison, so it renders exactly like content
with no group id at all. `groupParallelContent` demotes such a group into
the sequential flow, and `ContentParts`, `useContentMetadata`,
`MultiMessage` and `groupActivityPhases`'s `foldable` all ask the shared
predicate. Fixing it client-side repairs persisted history too.

Two adjacent bugs the demotion exposed in `ParallelContentRenderer`:
a sequential part landing between the lanes' first and last index was
dropped from the message entirely, and a section holding only placeholders
rendered every part twice.

Five existing fixtures asserted the parallel path with a single agent
(some with a string `groupId` that never matched `groupId?: number`);
they now carry a second agent so they keep testing lanes.

* 🧹 refactor: Narrow the Lane Part Instead of Asserting Its Shape

`TMessageContentParts` already carries `agentId`, so the assertion was
only standing in for the undefined check the optional chain now does.

* 🧵 fix: Keep Lane Identity and Transcript Order Across Sections

Two P2s from review, both about content that a lane group does not own.

**Lane cardinality belongs to the message, not the slice.** A completed
server phase marker partitions the transcript, and `renderSegment` hands
each slice to its own `ContentPartsBody`. A slice can hold one agent of a
genuine two-agent group while its sibling slice holds the other, so
counting agents locally demoted both and dropped the `SiblingHeader` that
told the reader which agent produced which answer. `parallelLaneGroups`
resolves the group ids once over the whole message; every slice and
`groupParallelContent` now ask that verdict instead of recounting.

**Sequential content between two sections rendered after both.** The
before/after split was global: with group 1, an ordinary part, then group
2, the part fell into `after` and the transcript read group 1, group 2,
part. Sequential parts are now laid out around EACH section, so the part
renders in place. A part landing inside a section's own index range still
falls to the next block — it has no column and no slot inside one, which
is the tradeoff the earlier `>= minParallelIdx` bound already made to stop
dropping it entirely.

* 🔢 fix: Count Only Attributed Lanes, and Scan for Them Once

Round 2 of review, two findings.

**An unattributed part is not a second agent.** `agentId` and `groupId` are
independently optional on a run step — `getStepMetadata` builds both from
whatever the step carries and the writer sets each only when present — so a
group can hold a part the server never attributed. It shares the `unknown`
sentinel column, and counting that sentinel let one agent plus one
metadata-less part pass the two-lane threshold, leaving exactly the
single-agent turn this branch exists to demote in the parallel UI.
`attributedLaneCount` discounts the sentinel, `groupParallelContent` counts
claimed columns the same way, and the sentinel is now one exported constant
instead of a literal on each side.

**One lane scan per render, not two.** `groupActivityPhases` was scanning
the whole message for lanes to decide `foldable` while `ContentParts` was
scanning it again for `parallelLaneGroups`, on a path that re-runs for
every streamed delta. The scan is now computed once and handed down; a set
built from the same content answers `foldable` by its size.

* 🧮 fix: Ignore Handoff Markers in Lanes, and Scan a Message Once

Round 3 of review, two findings.

**A handoff marker is not a second lane.** `useStepHandler` stamps an
`AGENT_UPDATE` part with `{ agentId: <destination>, groupId: 1 }` even
though the destination's own run steps carry no group id, so a single-lane
graph that later hands off held two agent ids in one group: the run's
output became one column and the marker became the other, at the exact
point the handoff happened. `isLaneMarkerPart` keeps a marker from claiming
a lane, and `groupParallelContent` mirrors it — a column claims its lane
only when it holds output, except a placeholder column, which claims with
no parts at all so a dual run shows both agents from the first render.

**One scan per message, not three.** `MultiMessage`, `useContentMetadata`
and `ContentParts` each traverse the same array in a single render pass,
allocating a map and a set per call, and `content` is rebuilt on every
streamed delta. `laneAgentsByGroup` now memoizes on the content array
itself: the three callers collapse to one traversal, and the cache is
exactly as fresh as the render, since a delta that kept the array identity
would not re-render either. Entries die with the array that keys them.
2026-09-03 23:46:25 -04:00
Dustin Healy
2ffa9ec420
➕ feat: Add Agent Management Creation Endpoint (#15564)
* feat: add Agent Management creation endpoint

* 🧫 fix: Reject Null Agent Management Models (#15581)

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-03 23:45:04 -04:00
Danny Avila
fde46a62f9
🔙 refactor: Keep Pasted Text Return Action Visible (#15586)
The chip generated from a long paste offers returning its text to the composer
on the subtitle line, but the label only swapped in on `group-hover` /
`group-focus-within`. At rest the chip read as an inert "Plain Text" subtitle,
so the affordance went unfound — and touch devices have no pointer to find it
with, which the `[@media(hover:none)]` override was already conceding.

The label is now the subtitle: stated at rest, underlined so a permanent line
of secondary text reads as a control rather than as a caption. The `aria-label`
that shadowed the visible text goes with the swap it existed for, leaving the
accessible name equal to what the label says.
2026-09-03 23:41:25 -04:00
Danny Avila
a9ce3d2b33
⏱️ test: Size the Subagent Lease TTL Above Shard Jitter (#15568)
The child-thread heartbeat fences itself when a renewal commits at or after
the deadline it was issued against, so a 500ms lease made this test's window
narrower than the scheduling stalls a loaded coverage shard produces: an
ordinary pause read as a lapse, the post-preparation gate refused the run, and
the assertion saw the provider never invoked while the lease row was still
held. Give the lease two seconds so the heartbeat, not the runner, decides.

Take the slow-preparation warning by its own delay as well, instead of
assuming it is the last timer scheduled before the test resumes.
2026-09-03 22:33:15 -04:00
Danny Avila
32e8724193
🏗️ fix: Wait Out Concurrent Index Builds on DocumentDB (#15563)
* 🏗️ fix: Wait Out Concurrent Index Builds on DocumentDB

Mongoose starts every compiled model's automatic index build in the
background as soon as the connection opens. The durable trigger delivery
boot step then issues an explicit createIndexes() for the same collection
while that build is still running. Amazon DocumentDB allows one index build
per collection and rejects the second with code 40333, which the retry
helper did not treat as transient, so the trigger service failed to
initialize and the post-listen catch exited the process on every boot.

createIndexesWithRetry now lets the model's automatic build settle before
building explicitly, and treats the single-build rejection as retryable
with a longer backoff so a peer replica's build of the same collection is
waited out instead of crash-looping the process.

Fixes #15556

* 🏗️ fix: Poll Instead of Backing Off While a Peer Holds the Index Build

A peer replica's index build time is data-dependent, so a fixed retry
budget either expires into the same process exit this change removes or
grows arbitrarily. The single-build conflict is now handled by polling at
a fixed interval with progress logging, unbounded unless a deadline is
supplied; other errors keep the existing backoff and fail-fast behaviour.
2026-09-03 22:28:27 -04:00
Lailson Henrique
1c2796b978
📮 fix: Validate Invite Email Claims (#15542)
* 🔒 fix: Require the email getInvite claims to validate

`findToken` builds its query from the fields it is given, so an absent
email is a lookup by token alone: the invite matches, `checkInviteUser`
deletes it, and the registration then fails on the missing field. The
invite is consumed and no account is created.

* 🎨 style: Sort spec imports by line length

---------

Co-authored-by: lailson henrique <dev@iagiliza.com.br>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-03 22:27:49 -04:00
Marco Beretta
bd31ecd129
✨ feat: add morphing state icons across the client (#14697)
* feat(client-ui): add morphing icon support

* feat(client-ui): morph shared state icons

* feat(chat-ui): morph chat action icons

* feat(client-ui): morph remaining action icons

* fix(client-ui): type morph icon test factory

* test(client): type agent detail fixture

* test(client-ui): guard morph icon accessibility defaults

* test(chat-ui): assert mermaid view toggle via morph icon

* style(chat-ui): sort mermaid header imports

* test(api): wait for rebuilt MCP session to finish connecting

The SSE conflict recovery test treated a new session id as a finished
reconnect. initialize assigns that id before connectClient emits
connected, so isConnected() can still be false. Wait for a live
connection on the rebuilt session instead.

* fix(client): clean up rebase fallout
2026-09-04 03:27:14 +02:00
Dustin Healy
2fe872d2ea
👀 feat: Add Agent Management Read Endpoints (#15561)
* feat: add Agent Management read endpoints

* fix: protect Agent management configuration reads

* fix: type Agent management list filters

* test: use typed Agent management assertions
2026-09-03 21:16:14 -04:00
Danny Avila
a6cf61abcf
🪵 feat: Add OpenTelemetry Logs Pipeline via Winston Bridge (#15565)
Exports application logs as OTel log records when OTEL_LOGS_ENABLED=true,
correlated with the active trace, and gates every signal explicitly so a
disabled signal never falls back to the SDK's default OTLP exporter.
Verified end to end over OTLP/gRPC against an in-process collector.
2026-09-03 21:16:09 -04:00
Dustin Healy
aec2121923
🔐 feat: Add M2M Authentication for Agent Management (#15555)
* feat: add M2M authentication for Agent Management

* fix: Harden Agent Management authentication

* fix: Preserve OIDC authentication safeguards

* fix: Normalize management principal IDs

* fix: Bound OIDC signing key lookups

* fix: Allow non-browser Agent Management clients
2026-09-03 20:07:00 -04:00
Danny Avila
04e4fd6b68
🏛️ feat: Establish Agent Management API Contract (#15545)
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 / publish (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
* feat: add agent management API contract

* fix: annotate agent management exports

* fix: strengthen agent projection typing

* fix: decouple agent responses from admission limits
2026-09-03 14:29:08 -04:00
Danny Avila
f0dfeed2b0
🐝 fix: Keep Polling for Owed Agent Successors (#15537)
* 🎯 fix: Ask Because a Successor Is Owed, Not Because One Might Be

The handover window guessed at how long admission takes. For a queued turn it
never had to: the backend reports the turn as `queued` or `claimed`, which is
positive evidence that a run is coming and that this client will not be the one
to start it. Declare that outright and keep the active-job list live for
exactly as long as it holds.

Read from the server's receipts rather than the chip projection, so admission
semantics stay in one place — `shouldPollAgentQueuedTurns`, shared with the
queue itself — instead of being restated here and drifting. `useSteering` owns
that fetch; subscribing with `enabled: false` observes its cache without
competing for it, and an unpopulated cache falls back to the window. It also
keeps this off Recoil, which the client is migrating away from.

The window stays for successors nothing local predicts — a background-tool
continuation has no receipt to report itself — and is now documented as the
fallback rather than the mechanism.

* 🔗 fix: Carry the Owed Successor Across the Receipt Handover

Codex found the gap in the deterministic path: the receipt that announces a
queued turn reports `admitted` for one fetch at most and is then dropped from
the projection — before the active-job list has necessarily observed the run
it started. If admission outlasted the handover window, the wall-clock
fallback was already spent, and a pane keyed on the receipt alone stopped
listening in exactly the window that mattered.

Two structural changes, so this is not another edge patched.

Owed is held, not read. Once a successor is known to be owed it stays owed
until it is delivered — this pane attaches, or the list names the conversation.
A bounded expiry backstops the case where neither ever happens, and it starts
only once the receipt has gone quiet, so a long wait behind an unadmitted turn
does not burn the window before the handover begins. `admitted` now counts as
owed: the receipt poll stops there because nothing is left to wait for, but an
unattached pane is in the opposite position, and it is the strongest evidence
that pane will get.

The receipt arms on its own. The arm had one announcement source, the list,
which can miss a run that starts and finishes between two polls. An owed turn
is a second source, and the status read the arm enables is the authoritative
answer either way: active attaches, inactive means the history refetch the arm
already performs is the repair. Neither source has to wait on the other.

* 🧹 chore: Sort Imports in useResumeOnLoad

* 🧭 fix: Deliver an Owed Successor Only When the Successor Itself Is Seen

Codex: the latch treated the pane's own live attachment as delivery of the
owed successor. But the successor is usually announced while its predecessor is
still attached — the turn was queued behind it — so the latch was cleared, or
never recorded, in exactly the window it exists for. Once that predecessor
closed and the receipt was dropped, a successor that started and finished
between two list polls was never seen.

Remember which generation was live when the turn became owed. Delivery is then
attachment to a different generation, or the list naming the conversation while
nothing of ours is live. The predecessor no longer counts.

Each half is proved against its own defect: reverting to the old predicate
fails the predecessor-attached case; dropping the generation-aware attach fails
the different-epoch case; and forgetting which generation was the predecessor
livelocks — the latch is set, "delivered" by the very generation it should have
excluded, cleared, and set again.

* 🧷 fix: Judge Successor Delivery by Generation, Re-Arm on Transitions, Expire Off-Screen

Three codex findings on the owed-successor latch, each a way the latch could
be cleared, never recorded, or held too long.

Delivery by generation only. The active-job list carries no generation
identity, so "the conversation is listed" was satisfying delivery while the
listed run was the predecessor — running elsewhere, unattached here — and the
latch was never recorded. The receipt knows the predecessor better than this
pane does: `expectedPredecessorCreatedAt` is stamped from the live epoch at
enqueue and `effectivePredecessorCreatedAt` is the boundary admission actually
consumed. Delivery is now attachment to a generation other than that boundary;
a turn still owed cannot have started, so a generation live when it is first
seen is its predecessor and refines the boundary rather than delivering.

Re-arm on transitions, not heartbeats. A `queued` receipt is refetched every
two seconds during a long wait and advances `dataUpdatedAt` without changing,
and the arm was keyed on that stamp — after the throttle it invalidated status
and history again and again while nothing had happened. It now keys on a
signature of receipt ids and statuses, and refetches history only when a run
is listed or the receipts actually transitioned.

Expire on an absolute window, off-screen too. The expiry effect skipped when
the latched conversation was not the one on screen, so navigating away held
the latch indefinitely and returning later reopened a fresh window. The window
now starts from `quietSince` — set when the receipt stops reporting or the
conversation leaves the screen — and is never pushed back by a remount.

Each change is proved against its own defect: re-adding list-based delivery
fails the listed-predecessor case; removing the transition gate fails the
heartbeat case; skipping expiry off-screen fails the absolute-window case; and
forgetting to learn a live predecessor livelocks the latch.

* 🧽 test: Restore the Admin Allowlist Override This Spec Leaks Into the Shard

The spec writes a per-user `mcpSettings.allowedDomains` override for the shared
primary user and never removes it. The list holds only its own fixture's
origin, and allowlist matching is protocol- and port-inclusive, so for the rest
of the shard every other MCP fixture is blocked: `e2e-oauth` fails inspection,
and an agent expecting its tools initializes into
AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE (503) instead of producing its OAuth
prompt.

Whether that bites depends only on which specs the shard runs afterwards, so it
surfaces as a deterministic failure in an unrelated spec — `mcp-oauth-resume`
failing an assertion that fires before any resume happens — which passes
whenever it runs first. A client-only PR whose path selection reordered the
shard hit it three times across two heads.

Delete the override in a `finally`, and confirm against the stored config
rather than by re-running `reinitialize`: the connection this test established
stays live, so reinitialize keeps succeeding once the allowlist is gone.

* 🩹 test: Read the Config Wrapper and Treat 404 as Cleared in the Override Teardown

`GET /api/admin/config/:principalType/:principalId` returns `{ config }`, and
deleting the last override removes the document, so the follow-up read is a
404 — the cleared state, not a read failure. The poll treated both as
unreadable and timed out.

* 🧷 fix: Remember Delivered Receipts, Restart the Grace on Delivery, Gate Reopening on Newer Data

Three more codex findings on the owed-successor latch.

Delivered receipts are remembered. An `admitted` receipt stays in the cache for
a fetch after the successor attaches, and it still counts as owed — so the
latch cleared on attachment and was re-recorded from the same receipt on the
next render, synchronously, until the cache moved on. That is React's
maximum-update-depth failure, reachable in the ordinary queued-turn handoff.
The receipt signature that delivered is recorded, and a receipt set with that
signature neither latches nor counts as owing a run; a genuinely new turn can.

Delivery restarts the fallback grace. The list may never have seen the run a
queued turn produced — it can start and finish between two polls — so its own
"recently active" clock is stale, and clearing the latch would have removed
the only reason left to poll. An unpredicted continuation after that run (a
background-tool dispatch finishing) would then go unnoticed until a focus or
reload. Observing the successor live now restarts the handover window.

The window reopens only on newer evidence. Returning to a conversation first
exposes the receipt cached before the turn went quiet, while the authoritative
refetch is still in flight; treating that as "still reporting" reset the
start of an expiry meant to be absolute, so repeated navigation could extend
it indefinitely. Only a server observation newer than the window's start
counts, for reopening and for the reporting check alike.

Each is proved against its defect: forgetting delivered receipts livelocks the
handoff test; skipping the grace restart fails its assertion; reopening on any
report fails the stale-remount case.

* 🧷 fix: Track Delivery Per Turn, Ratchet the Boundary, Keep One Latch Per Conversation

Three more codex findings on the owed-successor latch, plus a bug the second
one's regression test exposed.

Delivery is tracked per queued turn. Two turns queued behind the same
generation deliver one at a time; marking the whole receipt set on the first
would let the still-attached first successor pass as delivery of the second.
While any owed turn is still waiting, nothing is delivered at all — a waiting
turn cannot have produced the live run, so that run is its predecessor.

The boundary ratchets forward. A receipt can name the chain's root long after
a sibling's successor became the link the turn actually waits behind, and the
latch learns that link while the turn waits. Taking the receipt's boundary
first would have "delivered" the second turn on its own predecessor. The
predecessor is the latest link known: the max of what the receipt says, what
the latch learned, and, while waiting, what is live. With no boundary known at
all, a live generation is not delivery; the bounded expiry is the fallback.

Latches are keyed by conversation. Navigating to a conversation that also owes
a run replaced the previous one's latch, cancelling its expiry and the history
repair it performs. Every latch keeps its own absolute window now, and expiry
runs for all of them regardless of which conversation is on screen.

The cherry-picked allowlist-override teardown is synced to its final form
(see #15543): cleanup always runs, only definitive answers end its poll, and
`reinitialize` is no longer used as the reverted signal.

Proofs: marking the whole receipt set is indistinguishable only because the
waiting rule already blocks it; forgetting to learn the live link, or letting
the receipt's root outrank it, both fail the two-turn case; a single-slot latch
fails the two-conversation case; delivering without a boundary fails its case.
2026-09-03 13:23:19 -04:00
Danny Avila
5f187d3a6c
🧽 test: Restore the Admin Allowlist Override This Spec Leaks Into the Shard (#15543)
* 🧽 test: Restore the Admin Allowlist Override This Spec Leaks Into the Shard

The spec writes a per-user `mcpSettings.allowedDomains` override for the shared
primary user and never removes it. The list holds only its own fixture's
origin, and allowlist matching is protocol- and port-inclusive, so for the rest
of the shard every other MCP fixture is blocked: `e2e-oauth` fails inspection,
and an agent expecting its tools initializes into
AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE (503) instead of producing its OAuth
prompt.

Whether that bites depends only on which specs the shard runs afterwards, so it
surfaces as a deterministic failure in an unrelated spec — `mcp-oauth-resume`
failing an assertion that fires before any resume happens — which passes
whenever it runs first. A client-only PR whose path selection reordered the
shard hit it three times across two heads.

Delete the override in a `finally`, and confirm against the stored config
rather than by re-running `reinitialize`: the connection this test established
stays live, so reinitialize keeps succeeding once the allowlist is gone.

* 🩹 test: Read the Config Wrapper and Treat 404 as Cleared in the Override Teardown

`GET /api/admin/config/:principalType/:principalId` returns `{ config }`, and
deleting the last override removes the document, so the follow-up read is a
404 — the cleared state, not a read failure. The poll treated both as
unreadable and timed out.

* 🧭 fix: Await Config Cache Invalidation Before Answering Admin Config Writes

Every admin config mutation fired `invalidateConfigCaches` and answered without
waiting for it. The write was durable, but the effective configuration — app
config, per-principal overrides, cached tools, the MCP config-source cache —
could still describe the previous overrides when the response arrived, so a
caller that read right after writing could observe state it had just changed.
That is exactly how the allowlist-override e2e's teardown could report a clean
shard while the leaked allowlist was still in force.

`invalidateConfigCaches` already settles every clear and only logs failures,
so awaiting it cannot fail the request; it only orders the response after the
attempt. The e2e teardown now leans on that instead of polling the config
document, brackets the baseline assertion inside its cleanup scope so an
interrupted attempt can still un-poison the shard on retry, and treats a 404
from cleanup as a failure only when the override was actually installed, so it
can never mask the error that prevented installing it.

* 🧽 test: Confirm the Effective Allowlist Reverted Instead of Awaiting Invalidation

Reverts the awaited cache invalidation in the admin config handlers. Codex was
right on both counts: `invalidateCachedTools` can wait up to 30 seconds on the
Redis tool-cache lock, so awaiting it could hold a committed write until the
client's timeout; and APP_CONFIG is per-container by default, so an await only
ever proves this replica reverted while another keeps the previous overrides
for up to the override TTL. A test-hygiene PR should not ship a product
contract it cannot keep.

The teardown instead confirms the effective allowlist with the cache-backed
read the test already relies on. `reinitialize` disconnects the user
connection and re-resolves the merged allowlists per request, so the baseline
`success: false` returning is the definitive signal that the leaked override no
longer governs connections. The window exceeds the merged-config cache TTL, so
even a missed invalidation converges rather than leaking into the next spec.
The baseline stays inside the cleanup scope and a 404 from cleanup is a failure
only when the override was actually installed.

* 🧽 test: Verify the Override Is Gone, Not That a Connected Server Re-Blocks

The previous teardown polled `reinitialize` for the baseline `success: false`.
CI showed it never returns: a server that has already connected keeps
re-initializing successfully for more than 90 seconds after the override is
removed — past the 60-second merged-config TTL — because its allow decision
is not on the path that poisoned the shard. That path is agent tool loading,
which consults the merged app config per request; it is in-memory in these
shards and cleared by the mutation's asynchronous invalidation, and the
downstream victim, `mcp-oauth-resume`, passes with this cleanup in place.

Cleanup now always runs (a leaked override from an interrupted attempt is the
exact state it exists to remove), confirms the override document is gone with
only 200-without-the-override or 404 as definitive answers (anything else
retries), treats a 404 as a failure only when this attempt installed the
override, and describes the PUT's invalidation as what it is: asynchronous.
2026-09-03 12:42:10 -04:00
Danny Avila
849e9bb40d
🧳 fix: Carry Logical Endpoints Through Agent Runs (#15538)
* fix(agents): preserve logical endpoints in run inputs

* docs(agents): clarify endpoint input compatibility

* chore: update @librechat/agents to version 3.7.19

* refactor(agents): use released endpoint input type
2026-09-03 11:35:19 -04:00
Danny Avila
ac55b5fad4
🧼 fix: Clear Model Spec Fields on Configured Selections (#15544) 2026-09-03 09:46:15 -04:00
Danny Avila
2d794a0439
💓 fix: Detect Silently Dead Redis Sockets With Deadline-Bounded Heartbeats (#15539)
* fix: Detect silently dead Redis sockets with deadline-bounded heartbeats

A Redis peer can vanish without a FIN or RST (a dropped NAT entry, a proxy
failover, a migrated VM). The socket stays `ready` from ioredis's point of
view, no error fires, and every command on it waits for the kernel's
retransmission timeout — about fifteen minutes at Linux defaults.

Observed in production: while the main client's socket was dead,
POST /api/agents hung until Cloudflare returned a 524; while the subscriber's
socket was dead, every attach failed with "Timed out synchronizing Redis
subscription" for sixteen minutes across five conversations.

`REDIS_PING_INTERVAL` could not help — it awaits ping() with no deadline, so
the probe just joins everything else that is waiting.

- Add startRedisHeartbeat: races a PING against a deadline and destroys the
  socket when the probe goes unanswered, so ioredis reconnects, replays queued
  commands, and re-subscribes a subscriber's channels.
- Destroy the stream rather than disconnect(true), whose FIN a vanished peer
  never acknowledges; cluster clients fall back to the regular reconnect.
- Add createIoRedisSubscriber so every dedicated subscriber gets an error
  listener and its own heartbeat, and route the generation-stream and both
  subagent-routing subscribers through it.
- Set ioredis keepAlive from REDIS_KEEP_ALIVE (default 10s).
- Add REDIS_PING_TIMEOUT, REDIS_SUBSCRIBER_PING_INTERVAL and REDIS_KEEP_ALIVE,
  documented in .env.example.
- Give the fake RESP server a silent mode and a SUBSCRIBE reply so the
  dead-peer case can be tested against real clients.

* fix: Probe each cluster node and refuse a non-positive heartbeat deadline

A cluster-level PING is routed to one node, so a healthy reply proved nothing
about the other sockets. The heartbeat now resolves its targets each tick —
every node for a cluster, the client itself otherwise — and races an
independent deadline per target, tearing down only the socket that stops
answering. A non-positive deadline disables the heartbeat with a warning
instead of forcing a reconnect on every tick, and the single-probe test no
longer depends on wall-clock slack.
2026-09-03 09:45:16 -04:00
Danny Avila
458c473d9e
📧 fix: Normalize Token Emails on Write, as Every Read Already Does (#15536)
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 / publish (push) Waiting to run
* 📧 fix: Normalize Token Emails on Write, as Every Read Already Does

`findToken` and `deleteTokens` both trim and lowercase the email before querying —
the comment on `findToken` even documents it as automatic — but nothing normalized
the write. A token created with mixed case or surrounding whitespace could therefore
never be found or deleted again: an invite issued to `User@Example.com` was
unredeemable.

Fixed on the schema rather than in a caller. `createInvite` is the caller that
surfaced it (#15529), but the asymmetry belongs to `Token`, so every writer had the
same trap and any new one would inherit it. `User.email` is already declared
`lowercase: true` in this package, so this only brings `Token.email` in line with the
convention beside it — and a schema-level setter cannot be bypassed the way a helper
can.

Existing documents are not migrated. A token already stored with mixed case stays
unfindable, exactly as it is today; only new writes are corrected.

Fixes #15529

* ✏️ fix: Correct the Schema Comment and Name the No-Email Test for What It Asserts

Both from Copilot's review.

`User.email` sets `lowercase` but not `trim`, so claiming this "mirrors" it overstated
the parallel — this change trims as well, because the read side does. The comment now
states the `findToken`/`deleteTokens` contract it actually follows.

The no-email test was named for `null` while omitting the field and asserting
`undefined`. Renamed to match, and a real null case added: `TokenCreateData.email` is
`string | undefined` so null is only reachable by writing the model directly, but both
read methods branch on `email === null`, so it is worth proving the new setters leave
that value alone.
2026-09-03 07:46:41 -04:00
Danny Avila
d346e5ab5e
🚰 fix: Record the Provider Drain When Shutdown Cuts a Generation Short (#15532)
* 🚰 fix: Record the Provider Drain When Shutdown Cuts a Generation Short

* fix: Await generation settlement on shutdown instead of publishing the drain

* fix: Spend the real shutdown budget and cover the initialization window

* fix: Keep the shutdown registration literal the index spec asserts on

* fix: Keep draining settlements registered while shutdown is waiting

* fix: Track provider executions at the begin/drain chokepoints instead of per caller

* fix: Open the provider tracker before the begin CAS and cap cluster workers to the primary deadline

* fix: Propagate the cluster deadline and release trackers on reconfigure and abandonment

* fix: Hold SIGTERM until the worker acknowledges the cluster deadline

* fix: Refuse provider starts after shutdown begins and harden the cluster deadline handoff

* test: Assert the multi-execution shutdown wait with both executions begun before shutdown
2026-09-03 07:11:38 -04:00
Danny Avila
f9f1b2fb95
✨ v0.8.8-rc2 (#14845)
Some checks failed
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 / publish (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
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Docker Dev Images Build / publish (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
* chore: prepare v0.8.8-rc2 release

* test: satisfy mongo meili static checks

* test: avoid unsafe logger spy assertion

* chore: refresh v0.8.8-rc2 release

* test: restore mongo meili test isolation

* chore: normalize v0.8.8-rc2 versions

* docs: refresh v0.8.8-rc2 release surfaces
2026-09-02 22:18:48 -04:00
Danny Avila
90cdcb384e
📦 chore: bump @librechat/agents to v3.7.17 (#15531) 2026-09-02 21:08:53 -04:00
Danny Avila
8a46ca070c
🧊 feat: Persist Context Fading Tier Across Agent Runs (#15505)
* 🧊 feat: Persist Context Fading Tier Across Agent Runs

Carry the pruner's latched context-fading tier from `@librechat/agents`
through `contextMeta` the way `calibrationRatio` already travels, so a
historical tool result keeps the same truncated bytes from one run to the
next and Anthropic's prefix-based prompt cache survives across turns
(danny-avila/agents#497).

- Add `IAgentFadingTier` to the message and conversation `contextMeta`
  types, schemas and the data-provider zod schema
- Add `isAgentFadingTier`, `resolvePersistableFadingTier` and
  `resolveRunContextMeta` in `packages/api`; a tier is persisted once it
  carries information (masking active or a budget below the window), and
  a latched tier is kept even at a neutral calibration ratio
- Seed `createRun({ fadingTier })` from the parent response's contextMeta
  regardless of encoding, since caps are character-based; the field rides
  the existing forward-compatible `runConfig` variable and older SDK
  versions ignore it, as does `Run.getFadingTier` being absent
- Accept a valid `fading` field in event actor context and reject a
  malformed one, matching the calibration checks
- Capture run context meta through one module-level helper for both
  completion paths instead of two inline copies

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

* 🧊 fix: Seed Fading Tier Across HITL Resume, Validate at the DB Layer

Address review findings on fading tier persistence:

- Leave the "is this tier worth persisting" decision to the SDK, whose
  `Run.getFadingTier()` now returns only informative tiers; comparing
  against the client's window misclassified untouched conversations
  whenever a reserve ratio was configured
- Carry `contextMeta` through the HITL pause projection (staged approval,
  job metadata, resume state) and seed the rebuilt client from it, so a
  paused turn resumes with the same tier and calibration instead of
  re-deriving a shallower one and rewriting the prefix
- Share run seeding between `chatCompletion` and `resumeCompletion`, and
  make capture tolerate partial runs and bare resume contexts
- Validate `contextMeta.fading` in `commitAgentEventActorState` with a
  shared `isAgentFadingTier` guard in data-schemas, so a malformed tier is
  rejected at the DB layer instead of silently cold-starting the actor
- Reuse `IAgentEventActorContextMeta` for `IMessage.contextMeta` instead of
  extending an inline duplicate

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

* 🧊 test: Cover Resume Seeding From Paused Context Meta

Assert the resume controller seeds the rebuilt client from the context meta
captured at the pause before rebuilding the run, and move the capture
helper's JSDoc back above its function.

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

* 🧹 style: Sort Imports in Fading Tier Plumbing

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

* 🧊 fix: Persist Fading Tier Through Redis Jobs and Branches

Address Codex and Copilot review findings on fading tier persistence:

- Deserialize `contextMeta` in `RedisJobStore`, whose explicit read mapper
  rebuilt every other pause field but this one, so Redis-backed
  deployments resumed with no tier to seed after every HITL pause
- Copy server-private `contextMeta` onto the assistant message created by
  `POST /api/messages/branch`, so the next turn seeds its pruner the same
  way it would from the parallel source response
- Constrain the `fading` subdocuments in both Mongoose schemas (version
  enum, positive budget, required fields) to match the zod schema
- Throw a distinct "fading tier is invalid" error from both context meta
  validators instead of reporting a calibration failure

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

* 🧊 fix: Carry Context Meta Through Stop and Re-Pause

Addresses the second Codex pass on the fading-tier persistence:

- A re-pause whose resumed segment has nothing persistable now clears the
  job's `contextMeta` in the same transition instead of leaving the first
  pause's calibration and tier for the next resume to seed from.
- The client publishes the run's live calibration and fading tier onto the
  job after each pre-invoke context snapshot (deduplicated on value), and
  the Stop path copies `jobData.contextMeta` onto the stopped response, so
  a follow-up from a stopped turn keeps the prompt prefix stable.
- Mid-run captures (HITL pause, Stop) read the graph's live state; `Run`
  refreshes its own getters only after the stream settles, so the pause
  used to persist the seeds it started from.

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

* 🧊 fix: Fence Context Meta Publication Ahead of Stop

A Stop handled by another replica reads the job before the owner is
signaled, so a fire-and-forget publish could lose the race, and a run
stopped before its first context snapshot never published the tier it
inherited. The client now publishes the inherited seed before the run
streams or resumes, and the context-usage handler awaits each
snapshot's publish so the write lands before the model call it
describes begins. Failures still only log; the pre-run publish is
optional-called so bare resume contexts keep working.

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

* 🧊 fix: Read Back Context Meta After the Abort Claim

The run owner publishes its context meta ahead of each model call and
awaits the write, so a publish landing between the abort's initial job
read and its terminal claim describes the call whose partial output the
abort snapshot carries. abortJob now reads the same-epoch job back after
the claim and content re-read and takes its context meta, so the stopped
response persists the tier that produced its bytes. The refresh is
best-effort and logs on failure like the content refresh beside it.

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

* 🧊 fix: Share In-Flight Context Meta Writes and Clear Neutral Live State

Two snapshot callbacks in a parallel-agent run can capture the same tier
before the first write settles; the publisher now caches the in-flight
promise per serialized value so the second caller awaits the same
durable write instead of treating an uncommitted value as published.
A live snapshot with nothing left to persist after an earlier publish now
writes a neutral record (ratio 1, no tier), since a running job's fields
cannot be deleted through the metadata writer, so a Stop no longer
persists an earlier non-neutral state onto the response. The pre-run
seed publish is unchanged and is told apart from live snapshots by a
flag from the context-usage sink.

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

* 🧊 chore: Drop Unused Rest Parameter in Context Meta Test

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

* 🧊 feat: Persist Per-Agent Fading Tiers as Compact Context Meta

Reconciles the host with @librechat/agents at 9aacad1a, where graph
history stays canonical and each Run derives a provider-only projection
from the latched tiers. LibreChat therefore persists only compact state:
the calibration ratio, the default agent's tier, and now the per-agent
tiers from Run.getFadingTiers(), stored as validated entries so agent IDs
never become MongoDB field names and restored onto a null-prototype
record for RunConfig.fadingTiers. Truncated messages, canonical tool
content, projection provenance and per-message truncation state are
never persisted; the SDK's latched flag is stripped on capture.

The per-agent map travels everywhere the single tier already did:
response contextMeta, event-actor checkpoint state, HITL pause and
resume job metadata in both stores, the Stop path, and branch creation.
A shared Mongoose definition keeps the message and conversation schemas
identical, the zod schema validates entries, and commitAgentEventActorState
rejects malformed maps. Comments no longer describe graph messages as
truncated; only the provider projection is.

Tests cover: tool results and inputs reach the persisted content parts
at full size; contextMeta carries exactly the compact tier and
calibration fields; a persisted snapshot round-trips into the seeded
RunConfig unchanged and prototype-safe; per-agent tiers survive pause,
Redis job round-trips, and event-actor context validation.

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

* 🧊 fix: Restore Event Actor Types and Publish Seeds Before Abortable Setup

The previous commit's doc rewording in the conversation types dropped the
event-actor interfaces that precede the fading tier, which broke the
declaration bundle in CI; they are restored unchanged.

The inherited context meta is now published onto the job as soon as the
parent's state is loaded and awaited at the top of chatCompletion and
resumeCompletion, ahead of run creation and the other abortable setup
stages, so a Stop during setup still persists the parent's tiers onto the
stopped response. The partial response saved when every subscriber
disconnects now carries the job's context meta like the Stop and pause
paths do. The branch route keeps the source's context meta in the saved
message but strips it from the HTTP response, matching client reads.

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

* 🧊 fix: Serialize Distinct Context Meta Publications

Overlapping snapshots from parallel agents could carry different tiers;
each started its own write and both job stores keep whichever write
finished last, so an older snapshot could overwrite a newer one. Each
distinct publication is now issued after the previous one settles, in
call order, so the newest snapshot is what a Stop reads back. Equal
values still share one write.

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

* 🧊 fix: Project Root Message Reads and Keep Inherited Meta on Setup Failure

The root GET /api/messages paths returned raw documents: the
single-message query read called getMessages without a projection and the
cursor page performed an unprojected find. Both now apply
CLIENT_MESSAGE_SELECT, with getMessagesByCursor accepting a select option,
so the persisted fading tiers never reach the client through the
paginated endpoint; the search-hydration read projects the same way.

A turn that loads a parent's context meta but fails before its run exists
used to replace the inherited meta with the capture of a missing run, so
the persisted error response lost the last valid tiers. Both finalizers
now keep the inherited meta when no run was created while a created run's
neutral state may still clear it.

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

* 🧊 refactor: Move Context Meta Publication Into packages/api

The publication coordinator (ordering, deduplication, in-flight sharing,
failure state) now lives in packages/api as createContextMetaPublisher,
with selectRunContextMetaToPublish deciding what a publication carries;
the legacy client keeps only the wiring to the job metadata writer. A
transiently failing write is retried with a short backoff before the
publication is reported and forgotten, so a snapshot's model call no
longer proceeds on the first rejection while the job holds the previous
record. The SDK swallows handler errors, so retrying is the only way to
strengthen the fence without failing the turn.

A terminal save now supplies contextMeta: null when the finished run has
nothing to carry, and saveMessage unsets a previously stored value in
that case, so a disconnect snapshot's record cannot outlive the run that
completed neutrally.

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

* 🧊 fix: Widen the saveMessage Method Type for the Null Unset

The exported method interface still typed contextMeta as the message
field, so the spec exercising the null unset failed typechecking; it now
mirrors the implementation signature.

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

* 🧊 fix: Publish Every Agent's Snapshot, Sanitize Search Hits, Unbound Agent IDs

The context-usage handler now awaits the run's context meta publish for
every ON_CONTEXT_USAGE event, hidden sequential agents included, while
the visible-snapshot bookkeeping and the client emission keep their gate;
a Stop before the next visible snapshot finds the tier a hidden agent's
model call latched. Search results are built from the hydrated hit minus
its server-private contextMeta, since Meilisearch hydration projects every
schema field. The per-agent tier entry guard no longer caps the agent ID
length: ephemeral agent IDs encode endpoint, model and sender without a
bound, and dropping such an entry would let that agent alone re-derive
its tier on the next turn. Also formats the publication spec so the
static checks pass.

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

* 🧊 fix: Unset Context Meta Atomically, Keep Publication State After a Failed Write

- `saveMessage` folds the `contextMeta` unset into the same update that
  persists the terminal response, on both the plain and the provenance-merge
  paths, so a failure between two writes can no longer leave a completed row
  carrying a disconnect snapshot's state.
- `createContextMetaPublisher` tracks whether any record has committed
  separately from the retryable latest publication, so `hasPublished` stays
  true after a later publication exhausts its retries and a following neutral
  snapshot still overwrites the earlier record on the job.

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

* 🧊 fix: Carry Context Meta Through the Resumable Stop Route and Neutral Resumed Completions

- The resumable Stop route copies the refreshed `jobData.contextMeta` onto the
  terminal assistant message it persists, as the abort middleware already
  does, so a stopped resumable response keeps the tiers that produced its
  partial output.
- A resumed run that completes with neutral context state now saves
  `contextMeta: null`, which unsets the paused segment's record instead of
  letting an omitted field keep it on the completed row.

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

* 🧊 fix: Trust Only Server-Authored Context Meta, Unset It on a Neutral Stop, Read It From the Job on Disconnect

- `POST /api/messages/:conversationId` and conversation imports drop any
  client-supplied `contextMeta`, and the run seeds its calibration and fading
  tiers only from a server-authored parent response, so a forged tier can never
  shape a provider projection.
- Both Stop paths save `contextMeta: jobData.contextMeta ?? null`, so a job that
  re-paused with neutral state unsets what an earlier pause stored on the row
  instead of leaving it behind.
- The disconnect partial save reads the same-epoch job record for the run's
  published context meta, since the client-facing resume snapshot never carries
  server-private state.

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

* 🧪 test: Mock the Job Store in the Resume Metadata Spec

The disconnect partial save now reads the same-epoch job record through
`GenerationJobManager.getJobStore()`, so the resume metadata spec's manager
mock provides it as the other disconnect spec already does.

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

* 🧊 fix: Keep Context Meta Off the POST Message Response

`POST /api/messages/:conversationId` returned the saved row verbatim, so a
client write against an existing server-authored message echoed the row's
stored context meta. The route now projects the row through the same client
projection the branch route uses, which drops the server-private field.

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

* 📦 chore: Bump @librechat/agents to 3.7.16 for Persisted Fading Tiers

3.7.16 is the first release that carries danny-avila/agents#497, so
`Run.getFadingTier()` / `getFadingTiers()` now report the latched tier
this branch persists and `RunConfig.fadingTier` / `fadingTiers` seed the
next run from it. The dependency set is unchanged from 3.7.15; only the
package entry moves.

Adds an SDK-backed round trip to the fading spec: a first pruner latches
an informative tier, `resolveRunContextMeta` reduces it to the persisted
shape, and a second pruner seeded from that shape reproduces the first
run's projected tool-exchange bytes under instruction and calibration
drift, while an unseeded pruner under the same drift relaxes its tier and
rewrites them.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-02 20:29:43 -04:00
Danny Avila
676cd512b0
🫙 fix: Drop Blank Content Blocks From Promptless Sends (#15530)
* 🫙 fix: Drop Blank Content Blocks From Promptless Sends

* fix: Preserve the promptless turn instead of dropping it
2026-09-02 20:15:00 -04:00
Danny Avila
790f2b3153
⚡ feat: Add Gemini 3.8 Flash Support (#15519)
Some checks failed
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 / publish (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) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* ⚡ feat: Add Gemini 3.8 Flash Support

Adds first-class support for Google's Gemini 3.8 Flash (`gemini-3.8-flash`,
GA 2026-09-02) for both the Gemini API (AI Studio) and Google Cloud Gemini
Enterprise Agent Platform, following the Gemini 3.7 Flash integration (#14818).

The Flash-family handler that PR generalized already carries the shape 3.8
needs, so registering the model is a one-line rule rather than new behavior:
it inherits the strip of deprecated sampling params (temperature/topP/topK),
the rejected penalty params, and `thinkingBudget`, defaults to `medium`
thinking, and — like 3.7 — errors on `minimal`, so an explicit `minimal` is
substituted with the nearest supported level, `low`.

- Context window (1,048,576) in googleModels; API + cache pricing in tx.ts
- Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations
- Register the model in the Flash-family thinking-rule table
- Apply the same introductory pricing as 3.6/3.7 Flash ($0.75 in / $3.75 out /
  $0.075 cached, per 1M), reverting to $1.50 / $7.50 / $0.15 on 2027-01-01;
  the existing comments at both call sites now name 3.8 alongside 3.6/3.7
- Tests mirroring the 3.7 Flash coverage: thinking default, legacy-param strip,
  explicit level pass-through, `minimal` substitution, versioned aliases,
  context window, model-key mapping, and rates

Refs #15516

Ref: https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash
Ref: https://ai.google.dev/gemini-api/docs/pricing

* 🔧 chore: Update @librechat/agents dependency to version 3.7.15 in package-lock.json and related package.json files
2026-09-02 15:06:40 -04:00
Danny Avila
29f738dea8
⏭️ feat: Honor an Interrupt Before the Model Has Answered (#15491)
* ⏭️ feat: Honor an Interrupt Before the Model Has Answered

An interrupt armed while the model is still thinking sat as "Interrupting"
until the entire turn finished, then landed as a terminal continuation. The
cause is upstream: the SDK reads `shouldPreempt` once per streamed chunk, and
its seal gate requires non-empty text — so a silent provider is never polled at
all, and a reasoning-only turn is never sealable.

`@librechat/agents` gains a discard-and-restart path for exactly that window
(`HOOK_PREEMPT_RESTART_CAPABLE`). This wires the host half:

- `GenerationJobManager.subscribePreempt` registers run-scoped wake listeners,
  notified after an ACCEPTED arm — local or cross-replica, since both land in
  `armPreemptIds`. Fenced on `createdAt` like every other preempt entry point,
  and a throwing listener never fails the arm a queued steer depends on.
- `createSteerPreemptPoll` hands that channel to the SDK as
  `StreamPreemption.subscribe`, gated on a new `isSteerPreemptRestartSupported`
  probe so an SDK that cannot act on a wake is never handed one.

The wake is a hint only: `isPreemptRequested` stays the single authority, so
the level-triggered contract the seal path depends on is unchanged.

Inert until `@librechat/agents` publishes the restart contract — the probe
returns false against the pinned 3.7.11 and the run keeps today's behavior.

* 🩹 fix: Replay an Already-Armed Preempt on Subscribe

Preempt requests are level-triggered, and the window between a job becoming
steerable and the SDK installing its model-attempt listener is real. An
interrupt landing there was recorded with no callbacks to notify, and on a
silent or reasoning-only turn no later chunk poll may ever run — leaving the
request to wait out the whole turn, the exact stall this channel removes.

`subscribePreempt` now replays an existing arm to the newly registered listener
only, after registration: waking the whole set would re-notify runs that
already looked, and waking before registration would leave a concurrent arm
with nowhere to land. The `createdAt` fence is unchanged and still gates the
replay.

The SDK also reads the level flag once at each attempt's start, so this is
belt-and-braces rather than the sole cover — but it makes the host correct on
its own terms instead of resting on when the SDK happens to look.

* 🔧 chore: Update @librechat/agents dependency to version 3.7.14 in package-lock.json and related package.json files

* 🔧 chore: Update dependencies in package-lock.json and package.json

- Bump versions for @humanfs/core (0.19.1 to 0.19.2), @humanfs/node (0.16.6 to 0.16.8), @xmldom/xmldom (0.8.13 to 0.8.15), and qs (6.15.2 to 6.16.0).
- Add @humanfs/types (0.15.0) as a new dependency for @humanfs/node.
- Update dependencies for side-channel and side-channel-list to their latest versions.
2026-09-02 13:35:52 -04:00
Danny Avila
0219513115
🛑 fix: Forward Run Abort Signal to Foreground Tool Calls (#15517)
* 🛑 fix: Forward Run Abort Signal to Foreground Tool Calls

Stopping a generation left in-flight tool calls running on the far side.
For MCP over streamable-http the external server kept burning CPU until
the tool finished or timed out, with no `notifications/cancelled` ever
sent.

The MCP layer was never the problem: `MCPManager.callTool` already
spreads `options.signal` into `client.request`, and the SDK's
`Protocol.request` sends `notifications/cancelled` the moment that signal
aborts. The signal simply never arrived.

Agent tools do not execute inside LangGraph's ToolNode — `eventDrivenMode`
dispatches `on_tool_execute` and `createToolExecuteHandler` invokes them.
The agents SDK puts the run's abort signal on the batch request
(`ToolExecuteBatchRequest.signal`, documented as one handlers should
forward), but the handler destructured every field except `signal` and
built its invoke config without it. `config.signal` was therefore
`undefined` in every foreground tool, so `createMCPTool` derived no
signal and the SDK registered no abort listener.

Forward it. This restores cancellation for all signal-aware foreground
tools, not just MCP. The detached background invoke keeps its own
controller, since that work deliberately outlives the turn.

An aborted call now rejects promptly and resolves an error result rather
than hanging, so the abort is logged at debug instead of as a tool error
— a user pressing Stop should not spray the error log.

* 🔇 fix: Keep Cancelled Tool Calls on the Filtered Path and Out of Error Logs

Addresses both findings from the Codex review of 48fded564a.

P1 — the cancellation branch returned before `filteredToolOutputResult`,
so with tool-output filtering configured an aborted call's error text
reached the turn uninspected. Worse, `runSignal.aborted` says the run is
over, not that this rejection was the cancellation: an unrelated failure
racing the Stop took the same unfiltered exit. Cancellation now only
selects the log level; filtering and the result shape are unchanged.

P2 — `createMCPTool` logs its own error before wrapping and rethrowing,
so every Stop during an MCP call still emitted an error-level MCP entry
and could pollute operational alerts. That catch now recognizes the abort
and logs at debug; the wrapped message still reaches the turn.

Both are covered by tests that fail against the previous commit.

* 🎯 fix: Require an Abort-Shaped Error and Spare Shared OAuth Flows

Addresses both findings from the Codex review of b3c67082cd.

An aborted run signal proves the turn is over, not that the rejection in
hand was the cancellation — a permission, OAuth, or upstream failure can
reject in the same tick a user presses Stop. Both quiet-log branches now
require the error to look like an abort as well, so a real failure racing
the Stop stays at error level and visible to operational alerts.
`isAbortError` moves to `@librechat/api` and the copy in
`abortMiddleware` is retired, keeping one implementation. Its spec pulls
the real function through its partial package mock rather than asserting
a stub.

Forwarding the run signal also reached ActionService, where two of the
three OAuth flows are keyed `userId:action_id` and so outlive the run
that opened them: a second run for the same action joins the very same
record, and the browser's OAuth callback reads its metadata to exchange
the code. `monitorFlow` deletes the key when a waiter's signal aborts, so
one Stop could strand a concurrent run and discard an authorization the
user had already granted. Those two flows no longer take the signal; the
run-scoped login flow, keyed by thread and run, still does.

Every guard is covered by a test that fails when the guard is removed.

* 🧷 fix: Detach the Stopped OAuth Waiter Instead of Dropping Its Signal

Addresses the Codex review of d53ff170ab.

Withholding the run signal from the shared Action OAuth flows protected
concurrent runs and the browser callback, but left this invocation
attached to the flow after the Stop. A callback landing late then resumed
`_call` straight into `preparedExecutor.execute` — a consequential API
request running after the user had already stopped the turn. Handing the
signal back is not the answer either, since `monitorFlow` deletes the
shared `userId:action_id` key on abort.

`detachOnAbort` separates the two: the caller stops waiting the moment
the signal aborts, while the shared flow runs on for whoever else needs
it — another run, or the callback exchanging its code. Late settlement of
the detached work is swallowed rather than surfacing as an unhandled
rejection.

Both shared flows now use it. The run-scoped login flow keeps the signal
directly, as nothing outside its run observes it.

* 🪪 fix: Keep an Action OAuth Abort an Abort

Addresses two of the three findings from the Codex review of 70fc0782b1.

Detaching the stopped waiter left the rejection to be misread on the way
out. The refresh catch treated it as a failed refresh and called
`requestLogin()`, emitting an OAuth prompt and opening pending
authorization state for a turn that had already ended. `requestLogin` and
the surrounding auth catch relabelled it `Failed to authenticate OAuth
tool` / `Authentication failed`, and the outer catch handed that to
`logAxiosError` — so an ordinary Stop produced two error-level entries
and returned failure text as the tool's result. Each boundary now lets an
abort through unchanged, and the outer catch logs it at debug and
rethrows so it stays a cancellation end to end.

Deferred: waiter-only cancellation in `FlowStateManager`, so a detached
`monitorFlow` stops polling instead of running out the flow's TTL. That
poller is bounded at three minutes and lives exactly as long as it does
for a waiter that never left, so it changes no lifetime this branch
introduced; giving `FlowStateManager` a non-destructive abort mode is its
own change, shared with MCP and indexSync.

* 🤫 fix: Classify the Abort at the MCPManager Boundary Too

Addresses the Codex review of 9bcf1aa57e.

`MCPManager.callTool` logs every rejection at error level before
rethrowing to `createMCPTool`, so a user Stop still emitted an MCP error
from the inner boundary even after the outer one learned to recognize it.
The same signal-plus-abort-shape classification now applies here.

The review also noted, correctly, that the existing test could not have
caught this: it replaces `getMCPManager` wholesale, so it only ever
exercised the outer logger. The new cases drive the real manager with a
connection whose `client.request` rejects the way the SDK does once a
request signal aborts, and cover both the cancellation and a genuine
failure racing the Stop.

* 🧹 chore: Restore Import Order in the Agents Tool Handler
2026-09-02 13:32:33 -04:00
Dustin Healy
0b96fb9f70
🪪 feat: Propagate Tenant to Agent Model Headers (#15521)
* fix: propagate tenant to agent model headers

* fix: propagate tenant across agent model calls
2026-09-02 13:25:38 -04:00
Danny Avila
d5b2a8580e
📖 feat: Add Claude Fable 5.1 Support (#15512)
Some checks are pending
Docker Dev Images Build / publish (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
Claude Fable 5.1 (`claude-fable-5-1`, GA 2026-09-01) succeeds Claude Fable 5
at the same input/output prices, with a 1M context window and 128K max output.
The Mythos-class helpers added for Fable 5 already match it — `isMythosClassModel`
tests `claude-(?:fable|mythos)[-.]?\d`, so adaptive thinking, summarized-display
opt-in, sampling omission, 1M context, the 128K output clamp, prompt caching,
the Vertex multi-region gate, and the Bedrock PDF exemption all cover 5.1
without a code change. What was missing was registration and pricing.

Cache reads are the one rate that differs, and the substring matcher would have
silently gotten it wrong: `claude-fable-5-1` falls through to the `claude-fable-5`
key, which prices cache hits at the usual 0.1x base input ($1/MTok). Fable 5.1
and Mythos 5.1 price them at 0.025x ($0.25/MTok), so every cache read would have
billed 4x — on exactly the long agentic sessions this model is built for.

- Register `claude-fable-5-1` in the shared Anthropic list and
  `global.anthropic.claude-fable-5-1` in the Bedrock list
- Add 1M context / 128K output entries for `claude-fable-5-1` and
  `claude-mythos-5-1`
- Add $10/$50 rates and `{ write: 12.5, read: 0.25 }` cache rates for both
- Update the `.env.example` and Vertex `librechat.example.yaml` examples
- Test that 5.1 resolves to its own key rather than collapsing onto 5.0, across
  token maps, pricing, cache rates, and Mythos-class detection

The three documented breaking changes from Fable 5 need no handling here:
LibreChat never sends a forced `tool_choice` on the Anthropic path (the agents
SDK omits the param when unset, so the API default `auto` applies); thinking
blocks an earlier model can't read are dropped server-side, unbilled; and
history is rebuilt from stored content parts rather than replayed signed
thinking blocks, so the prefix-binding check has nothing to reject.

Refs #15506
2026-09-02 10:45:54 -04:00
Danny Avila
1c7a172321
🕹️ feat: Configure BYOM Tool Permissions (#15504)
* feat: add configurable BYOM permissions

* test: cover BYOM permission settings live

* fix: harden BYOM permission settings

* perf: reuse principals for environment discovery

* chore: normalize BYOM settings style

* fix: Align BYOM Discovery and Admission

* fix: Harden BYOM Permission Boundaries

* fix: Align BYOM Skill Approval Routing

* fix: Preserve Lazy BYOM Approval Capabilities

* perf: Parallelize Cached Environment Reads
2026-09-02 10:38:00 -04:00
Danny Avila
0cb1118e08
📱 test: Add Mobile Chat Performance Benchmark (#15510)
* test: add mobile chat performance benchmark

* fix: isolate mobile benchmark measurements
2026-09-02 10:36:33 -04:00
Danny Avila
057039570b
🔭 feat: Audit PII Filter Findings (#15508)
* feat: add audit-only content filter action

* refactor: align filter action typing and tests

* fix: preserve audit-only filter behavior

* fix: enforce audit mode across content paths

* fix: honor audit mode for canonical files
2026-09-02 10:36:06 -04:00
Danny Avila
a20048ab2d
🫱 fix: Keep Polling Through Queued Run Handoffs (#15509)
Queue a message during a run — queue, not steer — and the row is server-owned:
`useQueueDrain` deliberately declines the boundary (`serverOwnsBoundary`) and
the backend admits the successor itself. So the client has to hear about that
run through the active-job list, exactly like a background-tool continuation.

It cannot. Finishing a run empties the list — the client removes its own job
optimistically the moment FINAL lands — and the poll is disabled on an empty
list, so it goes quiet at precisely the moment the successor is admitted. With
the tab already focused there is no focus refetch either, which is why this
reproduces on desktop and not only on mobile. The announcement never arrives,
so the re-arm never runs and the pane sits on the previous turn until a reload.

Keep asking for a bounded window after the last known job ends. The window is
module-scoped rather than per-observer because this is one shared query and a
component mounting mid-handover should inherit the grace, not restart it.

This is the other half of the previous commits: without the re-arm the
announcement lands on a pane that still reads its finished submission as an
attachment, and without this the announcement never lands at all. Neither half
is observable alone.
2026-09-02 09:38:42 -04:00
Danny Avila
519e64769a
🚢 fix: Reconnect Keyv Redis After Replica Failover (#15502)
* 🔁 fix: Recover Keyv Redis Client From READONLY Replica After Failover

When a Sentinel failover demotes the master a pod is connected to, the
ioredis client recovers through `reconnectOnError`, but the Keyv/node-redis
client never does: `socket.reconnectStrategy` only runs when the socket
closes, and a demoted replica keeps existing sockets open while rejecting
every write with `READONLY`. Every cache write through the Keyv client
(USER_PRINCIPALS, MCP catalog, server configs) then fails until restart.

- Add a READONLY recovery hook for the standalone node-redis client that
  tears the socket down and reconnects, re-resolving to the current master
- Debounce attempts with `REDIS_READONLY_RECOVERY_INTERVAL` (5s default)
  and retry on the next error, since a reconnect issued before the
  topology settles can land on the demoted node again
- Route all three surfaces where READONLY replies appear through the hook:
  the client error event, the Keyv cache error funnel, and Lua scripts
  evaluated via `evalKeyvRedisScript`, which reject straight to callers
- Skip cluster clients, which follow MOVED to the owning master already
- Cover the hook with a fake RESP server so real node-redis and Keyv code
  paths are exercised against a node whose role flips mid-connection

Fixes #15500

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

* 🛟 fix: Harden READONLY Recovery Retry, Clear Path, and Clock

Address review findings on the READONLY failover recovery:

- Retry on the next error of any kind after a failed reconnect. A failed
  `connect()` leaves the client closed, so later failures surface as
  ClientClosedError rather than READONLY and the hook never fired again
- Route the namespace `clear()` override through recovery. It deletes
  through the raw client, bypassing the Keyv error funnel, so a READONLY
  rejection there previously left the pod stranded
- Debounce on an injectable monotonic clock (`performance.now`) instead of
  `Date.now`, so wall-clock steps cannot suppress or duplicate attempts;
  the interval test drives the clock instead of sleeping
- Reuse `isReadonlyReplicaError` in the ioredis `reconnectOnError` hook so
  READONLY detection has one definition
- Document the in-flight command rejection that a socket teardown implies

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

* 🛟 fix: Gate READONLY Recovery on Client State

Address the second review pass on the READONLY failover recovery:

- Act only on a client the hook can reason about: a READONLY reply on a
  ready socket, or a client its own failed reconnect left closed. While
  node-redis runs its own reconnect loop (open but not ready) the hook
  stays out, since a second `connect()` would start a concurrent loop
  that leaks sockets into one shared reply decoder
- Clear the pending retry whenever an error arrives on a ready client, so
  a client that Keyv auto-connected after a failed attempt is never torn
  down by a later unrelated error
- Drop the client error-event wiring: node-redis rejects command error
  replies straight to the command promise and emits `error` only from its
  socket and connect loop, so that listener could never see a READONLY
  reply and only fed connect-loop errors back into the hook
- Cover the connect-loop, self-recovered, and Keyv auto-connect outage
  cases, and a rejected namespace clear, with a SCAN-aware RESP server
- Add the new export to the `redisClients` mocks in the cacheFactory specs
2026-09-02 08:57:40 -04:00
Danny Avila
b1c56239f9
🧤 feat: Enforce Safe BYOM Tool Approvals (#15503)
* feat: add safe default approvals for BYOM code

* fix: preserve safe BYOM approval baseline
2026-09-02 08:53:16 -04:00
Danny Avila
434fa377c0
🗜 test: Clamp Recursive Child Inputs to Builder Types (#15498)
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 / publish (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 / publish (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-09-01 22:23:57 -04:00
Danny Avila
29e9d08163
🪛 chore: Align Vite Build Config With Native ESM (#15495)
* fix: Resolve Vite build warnings

* style: Sort token usage transition classes
2026-09-01 22:22:28 -04:00
Danny Avila
6372cb08ee
🧶 fix: Untangle Parallel Subagent Descriptor Paths (#15492)
* fix: isolate parallel subagent descriptor walks

* fix: preserve canonical subagent config identity

* fix: separate subagent graph assembly from path validation
2026-09-01 21:48:15 -04:00
Danny Avila
6d56d3b6fe
🧴 chore: Refresh Sanitizer and Selector Parser Dependencies (#15494)
* 🔧 fix: Update sanitize-html to version 2.17.7 in package.json and package-lock.json

* 🔧 chore: Update postcss-selector-parser to versions 6.1.3 and 7.1.3 in package.json
2026-09-01 21:38:08 -04:00
Marco Beretta
2cf1007f3c
🧺 feat: Preserve Partial Work at Tool Call Limits (#15486)
* feat: Handle reaching the agent tool-call limit gracefully

When the graph exhausts its per-turn step budget, persist the turn as
unfinished with finish_reason tool_call_limit instead of a red error.
Warn the model as the budget runs low, and offer Keep going or Answer now.

* docs: Add screenshots of the tool-call-limit notice

Light and dark captures of the unfinished-turn card for the PR description.

* style: Tighten the tool-call-limit notice card

Pin dismiss to the top-right with uniform rounding, use a slightly
smaller hit target, and round the card to xl. Refresh the PR screenshots.

* fix: Address Copilot review findings on the step-budget path

Skip empty and subagent-scoped PostToolBatch events when counting
rounds. Drop the double-cast in the notice test helper and coerce the
tool-call-limit finish reason in the Continue-gate spec.

* fix: Address Codex review findings on budget and dismiss

Pass the resolved recursion limit into the OpenAI-compatible
processStream path so the step-budget hook matches the graph.
Reset the notice dismiss state when the displayed message id changes.

* fix: Unblock CI typecheck and import sorting

Declare recursion_limit on the OpenAI service agent type, sort that
file's local imports, and route the notice test context through unknown
so tsc accepts the partial ChatContext fixture.

* fix: Sort remaining PR imports for static checks

* chore: Remove tool-call-limit notice screenshots

* fix: Keep recovery actions from consuming composer drafts

Pass empty file, skill, and quote overrides on Keep going and Answer
now so a staged compose is not attached or drained.
2026-09-01 21:38:02 -04:00
Danny Avila
71544f8236
🗿 chore: Upgrade gRPC to Prevent Receive Buffer Exhaustion (#15493) 2026-09-01 21:32:20 -04:00
csdivad
8a40e9f675
🔄 fix: Reindex MeiliSearch Documents After Indexed-Projection Changes (#15484)
* 🔄 fix: Reindex MeiliSearch Documents After Indexed-Projection Changes

- Introduce _meiliIndexSchemaVersion schema field and MEILI_INDEX_SCHEMA_VERSION constant
- Stamp schema version on single-document and batch index writes
- Query unindexed and stale-version documents in syncWithMeili
- Count stale documents in pendingIndexing in getSyncProgress
- Add spec coverage for reindexing stale-version documents

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* 🔄 test: use the registered Meili conversation model

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* 🔄 style: satisfy Prettier in Meili indexing progress counts

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-01 21:19:22 -04:00
csdivad
ce704f5c11
🔍 fix: Include Message-Matched Conversations in Sidebar Search (#15483)
* 🔍 fix: Include Message-Matched Conversations in Sidebar Search

- Union conversation and message index matches in getConvosByCursor
- Type searchMessages with SearchParams and SchemaWithMeiliMethods
- Inject searchMessages into createConversationMethods
- Bound sidebar Meili queries with 1000 limit and attributesToRetrieve
- Fall back to title-only matches when message search fails or is uninjected
- Escape filter user values in search queries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(search): preserve original conversation keys in sidebar hits

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* 🔍 fix: reuse the loaded conversation model

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* 🔍 fix: type the loaded conversation search model

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-01 21:17:11 -04:00
Danny Avila
164f146bdf
🛞 feat: Manage Personal Code Environment Lifecycles (#15369)
* feat: add self-service code worker pairing

* fix: enforce code worker principal boundaries

* fix: separate pairing control planes from execution

* fix: preserve principal worker routing

* fix: pin artifact traffic to code workers

* fix: authenticate code worker selection

* test: mock worker-aware artifact headers

* fix: harden self-service code worker routing

* fix: fail closed for principal code pairing

* feat: manage personal code environments

* fix: harden personal code environment lifecycle

* fix: satisfy backend lifecycle types

* fix: harden principal environment lifecycle and quotas

* fix: fence environment lifecycle races

* fix: reconcile code environment stack

* style: format code execution headers

* fix: reconcile code environment lifecycle

* fix: close self-service pairing gaps

* fix: close code environment reconciliation gaps

* fix: close code environment UI review findings

* fix: satisfy code environment type checks

* fix: route code priming to selected worker

* fix: harden code environment lifecycle fences

* fix: type lease renewal interval

* fix: harden environment lifecycle recovery

* fix: close lifecycle recovery races

* fix: type compensated agent timestamps

* fix: fence lifecycle cleanup by tenant and revision

* fix: scope environment revision fencing
2026-09-01 21:10:05 -04:00