Commit graph

5293 commits

Author SHA1 Message Date
Danny Avila
f0f82ec5fa
🧷 fix: Fence Server-Admitted Queue Boundaries (#15393)
* fix: fence server-admitted queue boundaries

* fix: preserve effective queue admission boundaries

* fix: fence queue recovery predecessor

* fix: unify queued boundary reconciliation

* fix: make queued turn receipt settlement monotonic

* fix: narrow queued receipt projection status

* fix: close queued turn settlement races

* fix: make queued admission lineage clock independent

* fix: centralize queued turn admission ownership

* test: type queued admission outcomes

* fix: make queued admission identity durable

* fix: carry queued admission identity end to end

* style: sort queued turn test imports

* fix: unify indeterminate admission ownership

* fix: migrate queued admission fence state

* fix: migrate every queued admission owner

* fix: surface queued admission reconciliation

* fix: preserve queued receipt evidence

* test: type queued receipt regression

* fix: stabilize queued receipt authority

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

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

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

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

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

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

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

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

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

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

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

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

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

* 🔁 fix: Address Codex Review on the Retarget Workflow

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

Review round 1, all four findings real:

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

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

Review round 2, all three findings real:

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

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

Review round 3, all four findings real:

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

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

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

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

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

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

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

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

Review round 6, both findings real:

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

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

* 🍃 fix: Materialize Authority Snapshot Namespaces Before Its Transaction

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

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

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

* 🍃 docs: Record the Authoritative Hardened Live Run

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #15359

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

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

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

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

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

Lockfile delta is limited to mermaid 11.16.1 -> 11.17.2, @mermaid-js/parser
1.2.0 -> 1.2.1, dompurify 3.4.12 -> 3.4.14, plus new fastdom + strictdom.
katex is unaffected: mermaid raised its floor to ^0.16.47, but the global
`katex: ^0.16.21` override supersedes it, and mermaid only ever calls
`katex.renderToString()`, which 0.16.28 provides.
2026-08-31 08:01:11 -04:00
Danny Avila
044894e8bb
🎛️ fix: Settle New Turns at the Bottom, Steady Status Dots, and Elapsed Time in the Panel (#15394)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 🎛️ fix: Settle New Turns at the Bottom, Steady Status Dots, and Elapsed Time in the Panel

* 🎛️ fix: Anchor Panel Timers, Order Turns by Trigger Time, Keep a Non-Color Status Cue
2026-08-30 23:11:00 -04:00
Danny Avila
14d4f2789d
🍃 fix: Send a Pure Inclusion Projection From the Legacy Receipt Sweep (#15390)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🍃 fix: Send a Pure Inclusion Projection From the Legacy Receipt Sweep

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

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

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

Review round 1, both findings real:

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

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

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

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

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

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

* test: update queued turn composition coverage

* fix: harden queued-turn lifecycle ownership

* fix: centralize queued-turn lifecycle ownership

* fix: close queued-turn lifecycle gaps

* fix: unify queued turn lifecycle transitions

* fix: fence queued-turn deletion admission

* fix: preserve queued-turn lifecycle ownership

* fix: close queued-turn reconciliation races

* fix: reconcile quarantined queued-turn admissions

* fix: annotate admission evidence default

* fix: deepen queued turn lifecycle admission

* fix: fence queued turn provider admission
2026-08-30 21:53:58 -04:00
Danny Avila
4de0a9341b
✍️ chore: Clarify That Interrupting a Steer Does Not Interrupt Reasoning (#15386)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
The interrupting receipt's hover implied the interrupt cuts in
immediately, but the seal waits for reasoning to finish. Say so, and
point at stopping the response as the way to interrupt reasoning.
2026-08-30 20:19:51 -04:00
Danny Avila
56d54a9416
📦 chore: bump vite to v8.2.2 (#15384)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Refreshes the lockfile to the version the existing `^8.0.16` range already
resolves to, so `npm install` and `npm ci` stop disagreeing about which Vite
builds the client. Carries rolldown 1.0.3 -> 1.2.6 with it.
2026-08-30 20:03:41 -04:00
Danny Avila
5fdc4f8656
🔒 chore: bump google.golang.org/grpc to v1.82.2 (GHSA-hrxh-6v49-42gf) (#15383)
Resolves the high-severity grpc-go advisory (xDS RBAC authorization
bypass, HTTP/2 rapid-reset mitigation bypass, xDS RBAC panic) flagged
against `otel/langfuse-fanout`. grpc is a transitive dependency there,
linked only because the OTLP collector protobufs carry generated gRPC
service stubs; the service itself speaks OTLP over HTTP and never
starts a gRPC server or uses xDS.

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

* 🧪 fix: Drop Narrating Comment From Detached Activity Spec
2026-08-30 19:53:59 -04:00
Danny Avila
1e4cae07c7
🧠 feat: Retain Subagent Reasoning Like Main Chat and Drop the Running Status Chip (#15379)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
2026-08-30 19:33:31 -04:00
Danny Avila
fcae1025c0
🧳 feat: Register Principal-Owned Code Environments (#15365)
* feat: add principal-owned code environments

* fix: address code environment CI coverage

* fix: expose principal code environments in endpoint config

* fix: reject missing code environment bodies

* fix: harden code environment lifecycle

* fix: revalidate principal code environments

* fix: synchronize code environment authorization

* fix: bind code environments to current principals

* fix: fail closed on code ACL cache errors

* fix: prevent code environment override shadowing

* fix: preserve principal environment defaults

* fix: suppress revoked code environment aliases

* fix: fail closed on environment augmentation

* fix: narrow code environment defaults

* fix: isolate code environment fallback

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

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

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

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

* 🤝 fix: Address Codex Round 1 on Steer Receipts

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

* 🤝 fix: Address Codex Round 2 on Steer Receipts

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

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

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

* 🤝 fix: Address Codex Round 4 on Steer Receipts

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

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

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

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

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

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

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

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

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

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

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

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

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

Correctness:

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

Structure and guard:

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

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

Second codex round, both findings real:

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

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

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

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

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

* fix: satisfy code environment type checks

* fix: secure code environment administration

* fix: isolate code pairing control plane

* fix: validate code pairing control responses

* fix: secure code pairing transport

* fix: validate code pairing wire format

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

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

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

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

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

* fix: Surface per-identity MCP instruction divergence

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

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

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

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

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

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

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

* test: Pin the MCP instruction context read path

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

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

* fix: Refuse MCP instruction backfill from mismatched configs

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

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

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

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

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

* fix: Scope deferred MCP instructions safely

* fix: Narrow optional Keyv namespace in Redis store detection

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

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

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

Codex round two, both verified before fixing:

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

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

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

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

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

CI caught two things local gates filtered past:

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

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

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

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

* fix: Make MCP aggregate writes atomic

* test: Fix Redis aggregate spy assertion

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

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

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

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

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

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

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

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

* fix: Preserve empty arrays in Redis aggregate mutations

* fix: Scope env-expanded MCP placeholders

* test: Harden Redis empty-array preservation

* style: Fix Redis cache static checks

* test: Narrow Redis empty-array fixtures

---------

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

* refactor: centralize remote agent execution lifecycle

* fix: preserve pre-settlement error rendering

* fix: preserve request-backed tool loading

* style: sort agent execution imports

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

* fix: include code environment in lazy agent type

* fix: harden stateful environment routing

* fix: complete code environment route isolation

* fix: declare code environment map type

* fix: preserve configured code execution routes

* fix: harden stateful environment updates

* test: assert route-scoped sandbox readiness

* fix: harden stateful environment lifecycle

* fix: isolate migrated code sessions

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

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

Three causes:

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

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

Output per full run:

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

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

* 🔧 fix: Settle Shutdown Timers Before Counting Them

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

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

* 🛡️ fix: Validate CONSOLE_LOG_LEVEL Instead of Trusting It

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

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

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

* 🎚️ fix: Let CONSOLE_LOG_LEVEL Outrank DEBUG_CONSOLE

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

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

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

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

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

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

* chore: bump agents sdk to v3.7.9

* fix: guard terminal steer admission
2026-08-30 11:54:17 -04:00
Danny Avila
180b56531a
fix: log config-blocked MCP recovery at debug instead of error (#15360)
A server blocked by the domain policy (or any configuration that makes
discovery impossible) fails recovery deterministically on every catalog
request, and each attempt logged at error level. In the e2e suite, where
e2e-http is deliberately policy-blocked to exercise the allowlist
override flow, this produced an error line every few seconds.

Discovery raises McpError InvalidRequest precisely for these
config-impossible cases, so classify it in discoverCandidate and log at
debug — the level this file already uses for config-proven skips —
keeping error for genuinely unexpected failures.
2026-08-30 10:55:58 -04:00
Danny Avila
240e9e920f
🧺 fix: Skip Oversized and Duplicate Files Instead of the Whole Batch (#15358)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🧺 fix: Skip Oversized and Duplicate Files Instead of the Whole Batch

Uploading several files at once failed entirely when any single one exceeded
`fileSizeLimit`: `validateFileSizes` returned false for the batch and
`discardProcessedUploads()` dropped every processed upload, valid files
included. Duplicates rejected the selection the same way.

Adds `partitionUploads`, which splits a selection into the files that may be
uploaded and the ones that cannot, and applies it at both validation points —
duplicates against the raw selection, sizes against the processed files whose
bytes resizing has already settled. Offenders are discarded individually and
named in a toast; the rest upload.

Per-file rules only: `totalSizeLimit` describes the batch as a whole, so
`validateFileSizes` still enforces it over whatever survives the partition.
When nothing survives, the batch keeps reporting the existing all-or-nothing
errors rather than per-file skip notices, so single-file uploads are unchanged.

* 🧮 fix: Count the Partitioned Selection Against the File Limit

Codex round 1.

`validateFiles` received the unpartitioned selection, so its
`fileList.length + files.size` check spent `fileLimit` slots on files the
partition was about to discard — with 9 of 10 slots taken, picking an
already-attached file alongside a new one rejected both. The partition now runs
first and hands `validateFiles` only the survivors, keeping the count rule in
one place. A selection that loses every file still passes the untouched list
through, so the batch-level checks report it in their usual order.

Also stops the partition tests allocating real buffers: one case built a 500 MiB
ArrayBuffer to assert a size-only branch.

* 🎚️ fix: Apply the File Count to the Files That Survive Processing

Codex round 2.

Round 1 stopped duplicates spending `fileLimit` slots, but the count still ran
before sizes were known: the pre-processing partition defers size checks, so an
oversized file was counted and then discarded. With one slot left, picking an
oversized file beside a valid one rejected both.

The count now runs where the survivors are finally known — after the
post-processing partition — through a `validateFileLimit` extracted from
`validateFiles`, so both callers share one rule and one message. The two
batch-composition rules the partition now owns, count and duplicates, sit behind
a single `skipBatchRules` flag in place of `skipDuplicateValidation`.

Selections that overshoot the limit are now processed before being rejected,
where they used to be turned away up front. That is the price of counting only
what will actually be attached, and matches how duplicates and sizes have always
been handled in this function.

* 🔕 fix: Hold Skip Notices Until the Batch Is Actually Uploading

Codex round 3.

The pre-processing partition announced its dropped duplicates as soon as
`validateFiles` passed, but the surviving files still had to clear the file
count and `totalSizeLimit` afterwards. A selection that failed there showed both
"Skipped duplicate file(s)" and the batch rejection, with nothing uploaded —
contradicting the notice's own contract that it describes a batch that is
otherwise still going.

Both partitions' skipped entries are now reported together, after the batch is
known to be uploading. A selection rejected in full reports itself through the
batch-level errors alone.
2026-08-30 09:04:13 -04:00
Danny Avila
2ae6c8aea9
🛎️ feat: Wake Agents on Background Tool Completion (#15350)
* feat: wake agents for background tool completion

* fix: isolate background completion contracts

* fix: anchor background completion identity

* fix: close background completion delivery gaps

* fix: preserve manual background polling

* fix: preserve legacy tool group identity

* fix: close background wakeup identity gaps

* test: type background wakeup enqueue mock

* fix: preserve background completion identity

* test: type activity phase fixture

* test: mock phase media query

* fix: arbitrate background result ownership

* fix: retain tool step routing helper

* test: expand phase groups before identity checks

* fix: preserve background completion ownership

* fix: bound durable background results

* fix: type wakeup input budget export

* chore: sort background handler imports

* fix: persist timed-out background completions

* fix: reconcile background completion ownership

* fix: type background completion capabilities

* fix: harden background completion terminalization

* test: type missing completion evidence

* fix: require evidence for completion retirement

* chore: satisfy background completion static checks

* test: cover automatic background completion wakeups

* test: assert background wakeup agent identity

* fix: wake capability-fenced trigger deliveries

* test: keep capability shield fixture public

* test: assert capability worker wakeup

* fix: close background completion ownership gaps

* test: satisfy completion lease static checks

* fix: preserve artifact completion wakeups

* fix: close background delivery recovery gaps

* fix: simplify background receipt guidance

* fix: recover dead background completion batches

* fix: fence background completion recovery

* fix: recheck background recovery ownership

* fix: fence unpublished background continuations

* test: await background message hydration
2026-08-30 08:56:41 -04:00
Danny Avila
e3ccaba5af
🛄 feat: Restore Compaction Guidance Across Continuations (#15356)
* 🧭 feat: preserve compaction guidance across continuations

* 🔧 fix: narrow persisted compaction fields
2026-08-30 08:13:24 -04:00
Danny Avila
b97d2868d4
🛑 feat: Cancel MCP Discovery Work at the Caller Deadline (#15353)
* feat: cancel discovery probes and tools/list at the caller deadline

deadlineMs could only be checked, never enforced: between checkpoints,
the health probe's ping ran to the SDK's 60s default, an in-flight
tools/list page ran to its page timeout, and a spent budget still paid
for credential preparation and a publication-order reservation before
anything noticed.

Derive one AbortSignal per discovery from the deadline and the caller's
own cancellation, and hand it to the work the SDK can genuinely cancel:
client.ping (and its fallback verification) via isConnected(signal), and
every tools/list page via fetchToolsSnapshot. An aborted probe reports
false for that caller only and never mutates connection state, so a
shared app connection stays usable for everyone else.

Phases that cannot be cancelled are no longer started once the budget is
spent: discovery returns before Graph preprocessing and token resolution
begin, and a snapshot returns incomplete before reserving publication
order. Teardown stays deliberately exempt — dispose() must finish.

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

* fix: keep aborted probes honest and combine caller signals

Codex round 1 on the abort threading found three gaps, all real:

- An aborted ping left lastConnectionCheckAt stamped, so a dead shared
  connection read as healthy for the whole CONNECTION_CHECK_TTL. The
  probe now restores the stamp when the caller signal caused the
  failure; the real-SDK test closes the server after an aborted probe
  and asserts the next caller sees it dead.
- The app-connection fast path built its signal from the deadline alone,
  so cancelling the originating request did not cancel a hung probe or
  tools/list there. One shared createDeadlineAbortSignal helper now
  combines both, used by the factory and the manager.
- The entry checkpoint could not see a budget that expired during
  credential preparation; discoverToolsInternal rechecks before token
  resolution begins.

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

* fix: consult the caller signal at discovery decision points

Round 2 found the pattern left half-applied: the signal cancels SDK
requests, but two orchestration points ignored it.

- An app-connection probe aborted by the caller fell through to
  discovery fallback, opening a fresh connection for a request that no
  longer exists. The manager now returns before the fallback when the
  caller signal is aborted or the budget spent, and the caller signal
  travels into non-OAuth discovery too — previously only OAuth
  connections could carry one, so signal moves to UserConnectionContext.
- fetchOrderedToolsSnapshot bounded the refresh wait by the deadline but
  not the signal, so a signal-only caller stayed blocked for the shared
  refresh's full cycle. settlesBefore now interrupts on either.

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

* fix: gate every discovery checkpoint on one cancellation predicate

Round 3 exposed the structural cause of the last two rounds: discovery
carries the same fact in two forms — a deadline number and a signal —
and each gate consulted only the deadline. isDiscoveryCancelled() is now
the one predicate at the static entry, the post-preparation entry, and
the pre-fallback gate, matching the manager gate and the snapshot gates
that already checked both.

connect() cannot carry the signal, so a signal-only abort previously
waited out the full connection timeout. connectWithinBudget races
connect against the timeout and the abort with owned cleanup: the timer
is always cleared, the abandoned attempt's rejection is swallowed, and
disposal hands whatever the dead connect still constructs to the
mid-connect disposal guard.
2026-08-30 08:12:04 -04:00
Danny Avila
41c92dfd06
⏱️ fix: Bound MCP Tool Discovery With a Caller Deadline (#15346)
* fix: bound MCP tool discovery end to end with a caller deadline

connectionTimeout bounds a single connect() only. Discovery then spends
it again on the unauthenticated fallback and hands tools/list its own
30s budget, so a caller working to a deadline had no way to cap the
whole operation.

Thread an optional absolute deadline through discovery into both
connect() and the tools/list walk, and dispose a timed-out authenticated
connection before the fallback opens its own socket — withTimeout does
not cancel the connect it abandoned, so the two were briefly concurrent.

Passive catalog recovery now sets one 3s per-server budget instead of a
per-attempt timeout it could spend several times over.

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

* fix: close deadline gaps in the app path, refresh wait, and disposal race

Codex review of the deadline threading found three real gaps:

- discoverServerTools returns through an app-connection fast path that
  never reached the factory, so its tools/list kept the 30s default.
- fetchOrderedToolsSnapshot checked the deadline before awaiting a
  refresh but the refresh runs on the connection's own budget, so an
  in-flight one could still hold a budgeted caller for that budget.
  Stop waiting on it rather than adopting it.
- connectClient never rechecked isDisposed after awaiting
  constructTransport, so a connect abandoned by its caller could
  assign a transport and connect after dispose() had already found
  nothing to close. Disposing before the fallback widened that window,
  so bound the attempt to its own disposal.

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

* fix: close the transport itself when disposal beats connect

Rewriting the disposal-race test against a real SDK server and transport
exposed a defect the mocked version could not see: client.close() only
closes a transport the client has adopted, and it has not adopted one
when disposal lands before client.connect(). The abandoned attempt's
session therefore stayed open and the reference was merely dropped.

Close the transport directly before closing the client. The test now
asserts the server observed the close, which fails against the previous
fix.
2026-08-30 07:44:52 -04:00
Danny Avila
70f735336d
🛎️ fix: Enroll Remote Agent Runs in the Generation Lifecycle (#15349)
* fix: enroll remote agent runs in generation lifecycle

* fix: close remote lifecycle ownership gaps

* fix: close remote conversation drain races

* fix: fence remote runs during conversation deletion

* fix: reconcile remote deletion and settlement races

* fix: complete owner deletion recovery

* fix: preserve remote cleanup receipts

* fix: consume deletion receipts before cleanup

* fix: expose idempotent deletion option

* chore: sort remote lifecycle imports
2026-08-30 07:14:13 -04:00
Dustin Healy
8fcab7e44f
🔄 fix: Recover Missing MCP Marketplace Catalogs (#15323)
* fix: recover missing MCP marketplace catalogs

* fix: make MCP catalog recovery passive

* test: type MCP catalog recovery fixtures

* fix: bound and back off passive MCP catalog recovery

Passive recovery runs inline on `GET /api/mcp/tools` and its results are
request-local by design, so every list request re-dialed the same cold
servers with the default connection timeout. Three limits keep that cost
proportional to what recovery can actually recover:

- Cap the discovery timeout at 5s instead of inheriting the connection
  default (`initTimeout ?? 30s`); a server configured to connect faster
  keeps its own shorter limit.
- Skip a server the config tier already marked `inspectionFailed`, leaving
  it to that tier's retry window rather than re-dialing it per request.
- Skip a server whose declared `customUserVars` are unset, matching the
  gate `reinitMCPServer` applies for issue #10969 — connecting without them
  fails auth, so the attempt is spent for nothing.

Servers that still fail discovery enter a one-minute per-process cooldown,
which is what stops an unreachable server from being re-dialed by every
subsequent list request. A server that recovers clears its own entry, and
expired entries are swept at most once per window so the map stays bounded.

Skipped servers render exactly as they did before recovery existed: present
in the catalog with an empty tool list.

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

* fix: bound passive MCP recovery by deadline, key cooldowns by config

Both follow-ups address the same mistake: recovery expressed its own
request-level constraints in terms borrowed from other layers.

`connectionTimeout` bounds one connection attempt, and
`MCPConnectionFactory.discoverToolsInternal` spends it twice — once on the
authenticated connection, then again in `attemptUnauthenticatedToolListing`
— so capping it bounded no total this layer could reason about. Recovery now
enforces its own wall-clock deadline per server with `withTimeout`, which
holds however many attempts the factory makes; `connectionTimeout` is left to
do only its own job, still honouring a shorter operator `initTimeout`. An
attempt abandoned by the deadline disposes its own connection when it
settles, and `Promise.race` keeps a handler on it, so a late rejection is
not unhandled.

A per-request budget now caps total recovery regardless of server count.
A server is dialed only if the remaining budget can fund a full deadline;
never dialing one is not evidence against it, so a skipped server records no
cooldown and a later request reaches it once those ahead are cached or
cooling down.

Cooldown identity now includes the publication generation — the same
effective-config identity the tool caches fence on — instead of just user and
server name. Correcting a server's URL or transport keys a new entry, so the
refetch the client issues on update is no longer skipped for up to a minute
by the previous configuration's failure.

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

* refactor: keep passive MCP recovery stateless and bounded by its own work

Reverts the cooldown, request budget and deadline race added in 257d5cf and
fc3e3c9, and keeps only the three stateless limits.

The tool cache refuses unfenced writes (`tools.ts`), and a discovery
connection owns no publication generation and is disposed, so a recovered
catalog cannot be retained by design. Those commits responded by building a
cache-shaped memory in front of it — per-process failure state, a scheduling
budget, an identity, an eviction sweep — and each round of review found
another way that hand-rolled cache differed from a real one: wrong identity
for configuration, wrong identity for credentials, no fairness across
requests, and a limiter slot released while its network operation was still
running. None of that machinery was asked for; all of it was compensation for
a result the architecture does not allow keeping.

Recovery is now stateless. It skips only what configuration alone proves
pointless — a server the config tier already marked `inspectionFailed`, and
one whose declared `customUserVars` are unset — and bounds the work itself
rather than racing it, so a limiter slot is held for exactly as long as its
network operation runs and the concurrency limit of three is real.

The attempt timeout is not a compromise: recovery exists for a server that is
reachable and authorized but whose catalog cache expired, and such a server
answers tools/list well inside 1.5s. Anything slower cannot be rescued here,
so failing fast costs nothing. The factory spends that value per attempt, so
a server's ceiling is it times the attempts made; the constant documents that
rather than hiding it behind a number tuned to today's attempt count.

Consequences that were bugs are now gone by construction: every cold server
is attempted on every request, so none is starved by those ahead of it, and
correcting a server's configuration or credentials takes effect on the next
refetch instead of waiting out a stale cooldown.

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

* fix: correct the inspection-failure skip and bound catalog fan-out

Three fixes that belong to this layer; a fourth issue does not, and is
described below.

The `inspectionFailed` skip was too broad. `MCPServersInitializer` stores a
YAML server that was unreachable at startup via `addServerStub`, which stamps
`source: 'yaml'`, and only config-tier entries get the timed retry in
`ensureSingleConfigServer`. Skipping every failed stub therefore hid a
recoverable server from the marketplace permanently — the exact state this
recovery exists to escape. It now defers only `source === 'config'`, matching
what `reinitMCPServer` already does.

Plugin auth is read only when some cold server actually declares
`customUserVars`, and only for those servers. The common unauthenticated case
no longer pays a MongoDB round trip whose result nothing can consume.

Snapshot refreshes are now bounded by the same limiter as discovery. They are
not local reads: both connection paths reach `fetchOrderedToolsSnapshot` and
issue a real `tools/list`, so a cache reset across many servers previously
burst unbounded outbound requests while discovery was capped at three.

Not fixed here, because it cannot be: `connectionTimeout` does not bound
discovery. It covers `connection.connect()` only, and `fetchToolsSnapshot`
then applies its own `TOOLS_LIST_TIMEOUT_MS` (30s) to `tools/list`, so a
server that connects fast and stalls while listing still holds its slot for
that window. The factory also does not cancel a timed-out connect before
starting the unauthenticated fallback. Bounding this end to end needs a
deadline threaded through `MCPConnectionFactory` into both `connect()` and
`fetchToolsSnapshot()`, which is a change to shared connection machinery
rather than to this caller. The constant's comment now states what it does
and does not bound instead of implying an end-to-end guarantee.

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

* fix: thread live-session OBO context into passive catalog discovery

The merge of #15334 sources OBO tokens from the live OpenID session via
request-boundary closures. Passive catalog recovery is a discovery call
site too; without these options an OBO server whose stored token went
stale fails recovery — the exact cold-catalog class this PR fixes.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-30 06:55:30 -04:00
Danny Avila
36f129089b
🔊 fix: Autoplay Latest Message for Browser TTS and short responses (#15347)
* 🔊 fix: Autoplay Latest Message for Browser TTS and short responses

Automatic playback never reached the Web Speech API and dropped audio for
short responses. Two independent defects:

Autoplay ignored the selected TTS engine. `ChatForm` mounted `StreamAudio`
whenever automatic playback was on, and that component always POSTs to
`/api/files/speech/tts`, so the Browser engine silently fell through to the
external endpoint and `speechSynthesis.speak()` was never called. The
engine-aware `useTextToSpeech` hook had no consumers. Autoplay now selects
its driver by engine the same way `MessageAudio` does, with the gate shared
between both drivers so they trigger on identical terms.

`MediaSourceAppender` stranded its queue. `tryAppendNextChunk` only ran from
`addData` and `updateend`, while `sourceopen` — which fires only once a media
element attaches the object URL — created the `SourceBuffer` without draining
what had queued up behind it. A response read in full before that event left
every chunk in the queue with no `updateend` to ever restart the drain, so the
element sat at `readyState: 0` and never played. Short responses lost the race;
longer ones streamed past `sourceopen` and worked. The handler now drains, and
`close()` (previously never called) ends the stream once the queue empties.

Also in the same path: the error branch had its timeout comparison inverted, so
real failures were logged as timeouts and swallowed; and the non-MSE fallback
that hands the element a finished blob lived inside the cache-only branch,
leaving browsers without MSE support for `audio/mpeg` with no audio at all
unless Cache TTS happened to be enabled.

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

* 🧹 style: Sort imports in autoplay files

`npm run sort-imports:check` on the changed files.

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

* 🔇 fix: End the media source when no audio ever arrives

Codex P2 on #15347. The `hasAppended` guard in `tryEndOfStream` was defensive
against a throw that cannot happen: `endOfStream()` only raises
`InvalidStateError` while the source is not open or a buffer is updating, both
of which the remaining guards already cover. Nothing about an empty buffer
throws.

The guard's actual effect was to strand the two cases it was meant to protect.
A TTS response that completes with zero bytes, or a read timeout that fires
before the first byte, both reach `close()` with the MediaSource URL already
attached to the element — and left it `open` forever, so the element waited on
a source that could never receive data and kept its streaming resources
2026-08-30 06:54:16 -04:00
Danny Avila
16c2bb4149
📬 feat: Establish Background Continuation Admission (#15348)
* feat: establish background continuation admission

* fix: preserve routed subagent wakeup controls

* fix: probe configured subagent task store
2026-08-30 06:51:36 -04:00
Danny Avila
4d5126976a
🪭 fix: Settle Parent Phase Cards Without Replaying the Entrance Fold (#15345)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
* 🪭 fix: Settle Parent Phase Cards Without Replaying the Entrance Fold

When the final event swaps in the server's compacted content and the
streamed-index stamp cannot pair the two arrays, every index-derived key
shifts and each already-settled phase card remounts as "new" — replaying
its entrance fold over content the reader already watched fold. Guard the
entrance by label text as well as render key, so a re-keyed marker whose
text was already on screen mounts settled.

Also stop tool-call groups from auto-expanding when they remount inside a
completed phase card: the phase summary already speaks for the activity,
so a group toggling open and shut mid-entrance only adds jitter.

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

* 🪭 fix: Count Repeated Phase Labels and Keep Pending Approvals Reachable

Codex review follow-up: a run can legitimately generate two phases with
identical summaries, so the entrance guard now counts label-text
occurrences instead of remembering texts — a grown count animates, a
settle-time re-key does not. And a pending approval overrides the
phase-internal collapse default, since a phase can resolve while an
approval inside it still blocks the run.

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

* 🪭 style: Sort ContentParts Imports for the Static Check

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

* 🪭 fix: Pair Repeated Phase Labels by Occurrence Across a Re-Key

Codex round-two follow-up: the aggregate label count applied to every
marker, so a settle that re-keys existing phases while landing a new
same-text one replayed every card's entrance. Each marker now carries its
occurrence ordinal and animates only past the previously rendered count
for its text.

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

* 🪭 fix: Engage the Phase Label Heuristic Only When a Key Vanishes

Codex round-three follow-up: key identity stays authoritative for pure
additions, so a marker filling out of order behind an already-rendered
same-text twin animates; the text-occurrence pairing now applies only
when a previously rendered key has vanished — the signature of a
settle-time re-key.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-30 01:25:37 -04:00
Ayush Verma
6425837269
👔 fix: Route Flux Finetuned Endpoints Regardless of Action (#15336)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
FluxAPI._call() only dispatched to generateFinetunedImage() when action
was explicitly "generate_finetuned". If a model set endpoint to
/v1/flux-pro-finetuned or /v1/flux-pro-1.1-ultra-finetuned while leaving
action at its default ("generate"), the request went through the plain
generate payload path instead, which never forwards finetune_id,
finetune_strength, or guidance. The finetune was silently dropped and a
finetuned endpoint was called without its required finetune_id.

Route by endpoint as well as action, and share the finetuned-endpoint
list between the dispatcher and generateFinetunedImage's own validation.

Fixes #15335
2026-08-30 00:04:06 -04:00
Danny Avila
fa913148fb
🔒 fix: Refresh MCP OBO Tokens From the Live OpenID Session (#15334)
* 🧊 fix: Inline-refresh OpenID session tokens at MCP OBO call time

Resolves the walk-away failure mode where MCP tool calls using OBO auth
fail with "No valid OpenID access token is available for OBO exchange"
after a user idles past their access-token lifetime. The strategy-time
snapshot on `user.federatedTokens` could expire mid-stream before
`resolveOboToken` ran, while `req.session.openidTokens` carried a still-
valid (or refreshable) token that nothing read.

- New OpenIDSessionRefresh service: per-user single-flighted closure that
  reads `req.session.openidTokens` at OBO time and inline-refreshes via
  `openid-client.refreshTokenGrant` when expired (30s skew), persisting
  via `req.session.save()`. No cookie writes (headers already flushed).
- `resolveOboToken` gains a required UpstreamTokenProvider parameter
  (typed as `() => Promise<OIDCTokens | null>`, reusing the shared shape
  from @librechat/data-schemas). Compile-time guarantee that every call
  site is updated.
- New `session_refresh_failed` OboTokenResolutionReason distinguishes
  "session expired and IdP rejected refresh" from "no upstream token
  ever existed."
- `req` threaded through createMCPTool/createMCPTools/createToolInstance
  to construct the closure with captured request, plus fail-closed
  guards in MCPConnectionFactory.getOboTokens and MCPManager.callTool
  when the closure isn't plumbed.
- Startup warning in MCPServersInitializer when OBO is configured but
  OPENID_REUSE_TOKENS is unset (the strategy populating
  user.federatedTokens is only registered under reuse, so OBO would
  fail every call without it).

Tests: 16 new in OpenIDSessionRefresh.spec.js; obo.spec.ts extended
for the new param + error reason; wiring smoke tests in MCPManager,
MCPConnectionFactory, MCPServersInitializer, and MCP.spec.js.

* 🛡️ fix: Harden OBO inline-refresh against token type and session edge cases

- Token-preference asymmetry: live-token reuse and expires_at derivation
  now strictly gate on the access_token, not the id_token. Added a
  required `tokenPreference` parameter on isLiveSessionTokenStillValid,
  buildOIDCTokensFromSession, and createOpenIDSessionTokenProvider
  so every call site is explicit. Dropped the bogus id_token-exp
  fallback in performIdpRefresh — id_token TTL is governed by IdP
  session policy and would mark a short-lived access_token reusable
  past its real lifetime.
- Missing req in /reinitialize route: the manual reconnect
  endpoint now forwards req into reinitMCPServer, so OBO servers can
  build a session-aware upstream-token closure instead of failing with
  missing_upstream_token.
- Single-flight key collisions: composed key as
  tenantId:openidIssuer:openidId:sessionId via getSingleFlightKey.
  Concurrent calls in the same session still coalesce; separate sessions
  never share an in-flight refresh, preventing refresh-token rotation
  from breaking sibling sessions and preventing cross-tenant token
  crossover when distinct users share an IdP sub.
- Opaque access token reuse): persist accessTokenExpiresAt
  (unix seconds, from tokenset.expires_in) on each refresh AND on initial
  login / SPA refresh in setOpenIDAuthTokens. New getAccessTokenExp
  helper falls back to it when the access token isn't a JWT, avoiding
  redundant inline refreshes for Microsoft Graph and Auth0 default
  audiences.
- Log hygiene: the single-flight key (containing sessionId,
  openidId, openidIssuer, tenantId) is now SHA-256-hashed in the
  "Joining in-flight refresh" debug log. Preserves cross-line correlation
  via a 12-char prefix without leaking credential or PII material.

Documented req.session.openidTokens shape contract via JSDoc typedef so
the new accessTokenExpiresAt field has a discoverable home alongside the
existing accessToken/idToken/refreshToken/expiresAt/lastRefreshedAt.

Tests: OpenIDSessionRefresh.spec.js up to 30 passing (added coverage for
opaque-token reuse, JWT-access-token-exp fallback, no-id_token-fallback
regression, cross-session no-coalesce, persistence on refresh, and a
guard against stale accessTokenExpiresAt carryover). AuthService.spec.js
adds two cases covering accessTokenExpiresAt persistence on login.
mcp.spec.js (route) gains a regression test asserting req flows into
reinitMCPServer.

* 🔍 fix: Detect OBO-only MCP admin config overrides

Admin Config overlays for YAML-defined MCP servers compare only
ADMIN_CONFIGURABLE_FIELDS to decide whether to lazy-init a config-tier override.
The OBO config field was added after that fingerprint list, so an override that
only added or changed `obo` was treated as unchanged YAML and skipped.

Include `obo` in the admin-configurable field list and add a regression test for
an OBO-only override.

* 🔊 fix: Mock MCP OAuth timeout in SDK integration test

MCPConnectionFactory.attemptToConnect reads mcpConfig.OAUTH_HANDLING_TIMEOUT
when building the OAuth connection timeout. The SDK OAuth integration test
mocked mcpConfig without that field, which made the timeout calculation produce
NaN and caused the test to fail before the OAuth refresh/start path completed.

Add OAUTH_HANDLING_TIMEOUT to the test mock.

* ♻️ refactor: Pass OBO upstream-token closure into MCP instead of req

Build the OpenID upstream-token provider at the request boundary and thread
only the closure through MCP handling, so the MCP service layer no longer
receives the raw Express request. The closure still reads/refreshes the live
session at tool-call time, preserving the walk-away recovery.

- Drop `req`/`capturedReq` from createMCPTools, createMCPTool, reconnectServer,
  createToolInstance, and reinitMCPServer; forward `upstreamTokenProvider`
  instead. Closure is constructed in loadTools, loadToolDefinitionsWrapper, and
  the reinitialize route, where req/res are in scope.
- OBO: fall back to user.federatedTokens when the provider yields no live
  session, so OIDC remote-agent calls (verified bearer, no session) still work.
- Inline refresh: mirror a rotated refresh token to the refreshToken cookie via
  a shared setRefreshTokenCookie helper, guarded by !res.headersSent (no-op on
  the streaming path; session copy stays authoritative).
- Single-flight: hydrate a joining request's own session from the resolved
  tokens so a later OBO call doesn't replay a rotated-away refresh token.

Addresses owner feedback and three review findings.

* 🔒 fix: Recover OIDC refresh-token rotation after SSE OBO refresh

When an inline OBO refresh rotates the OpenID refresh token after SSE headers
have already been sent, the browser refreshToken cookie cannot be updated. Store
a short-lived encrypted bridge from the stale cookie token to the rotated token
so /api/auth/refresh can recover after express-session loss.

Use the signed openid_user_id cookie to load user context for bridge validation,
retry only on invalid_grant, and delete the bridge only after the bridged refresh
succeeds.

* 🔨 fix: hydrate joined OIDC refresh sessions with stable refresh tokens

Update single-flight OIDC refresh joiners whenever refreshed access token
state changes, even if the IdP keeps the refresh token unchanged.

This prevents joined requests from retaining stale accessToken or
accessTokenExpiresAt values and redundantly refreshing later in the same run.

* 🌉 Persist OIDC refresh-token recovery bridges in MongoDB

Store SSE OBO refresh-token recovery bridges in MongoDB instead of
process-local memory so /api/auth/refresh can recover after worker
restarts or cross-worker routing.

Derive bridge expiry from REFRESH_TOKEN_EXPIRY so the recovery window
matches the stale refreshToken cookie it repairs, and delete bridges
after successful recovery.

* 🤝 Coordinate OIDC inline refreshes across workers

Add a short-lived Mongo-backed refresh-flight record so concurrent
OBO refreshes for the same OpenID session do not redeem the same
rotating refresh token on different workers.

The winning worker performs the IdP refresh and stores an encrypted
result; joiners wait for that result, hydrate their request session,
and return without calling the IdP.

* ⚓ Keep OpenID marker cookies aligned on inline refresh

Refresh token_provider and openid_user_id with the same expiry as the
rotated refreshToken cookie when an inline OBO refresh can still write
headers.

Share the marker-cookie writer with the normal OpenID auth refresh path
so the fallback /api/auth/refresh branch continues to recognize valid
OpenID refresh tokens after session expiry.

* 🔑 fix: include refresh token in OIDC local refresh flight key

Key the process-local OIDC refresh coalescing by the current session
refresh token, matching the Mongo-backed flight key. This prevents a
request with a newly rotated token from joining an older pending refresh
and inheriting its failure/result.

* 🌉 fix: store OIDC refresh bridge without cookie response

Treat missing or non-cookie responses like headers-sent streaming
responses during inline OIDC refresh. When the IdP rotates the refresh
token and cookies cannot be written, persist a recovery bridge so a later
/auth/refresh can recover after session expiry.

* 🫙 fix: preserve stale OIDC cookie bridge key

Track the refresh token last written to the browser cookie separately
from the current session refresh token. When inline OIDC refreshes rotate
tokens without a writable response, keep bridging from the browser-stale
token directly to the latest session token.

* 🙌 fix: keep OIDC bridge recovery success on cleanup failure

Make refresh-token bridge cleanup best-effort after a bridged OIDC
refresh succeeds. A transient delete failure now logs a warning but does
not convert the already-refreshed session and cookies into a 403 response.

* 📦 test: Exclude RefreshTokenBridge from tenant-isolation coverage

Add RefreshTokenBridge to the tenant-isolation coverage allowlist because
refresh bridge lookups run during unauthenticated OpenID refresh recovery.
The controller first recovers user context from the signed OpenID marker
cookie, then the bridge methods apply explicit user and tenant filters.

Ambient tenant isolation would bind this recovery path to request-local
tenant context that is not available at the point the stale cookie is being
resolved

* ⚡ Fix OpenID refresh flight retry and marker hydration

Allow failed OpenID refresh flights to be reclaimed immediately instead of pinning transient errors.

Preserve the browser refresh-token marker when joined refreshes hydrate session tokens from a shared flight result.

Stabilize AuthService tests by isolating mocked module imports from prior suites.

* 🛠️ fix: centralize OBO identity scoping

Add shared auth identity helpers for app user ids, OpenID subjects,
tenant ids, and normalized OpenID issuers.

Thread a non-placeholder-visible OBO identity context from the real
request user through MCP connection, tool-call, reinit, and refresh
paths. Keep tenantId and openidIssuer out of createSafeUser so MCP
user placeholders do not expose those fields.

Scope OBO token cache and in-flight exchange keys by tenant, issuer,
OpenID subject, scopes, and a SHA-256 hash of the upstream assertion.
This prevents cross-tenant/cross-issuer collisions and avoids reusing
tokens minted from stale rotated assertions.

Use the shared identity helpers for OpenID refresh-flight keys and
refresh-token bridge recovery records so related OBO refresh paths share
the same identity normalization rules.

The helper is intended for auth-boundary and credential-cache code, not
as a blanket replacement for ordinary app user id ownership checks.

* 🛠️ fix: preserve OIDC refresh-token sync on save failures

Sync OpenID refresh-token cookie/bridge state before persisting the
session so a transient session-store failure cannot lose an IdP-rotated
refresh token.

Also trigger sync when the session refresh token differs from the
browser refresh-token marker, not only when the current grant rotates
the token. This lets later writable refreshes repair stale browser
cookies left behind by SSE refreshes.

Route refresh bridge identity through the shared identity helper with
the threaded OBO identity context, falling back to request/user context
when needed.

Add regression coverage for session-save failures, stale browser cookie
repair, non-writable bridge storage, and shared-helper identity fallback.

* 🛠️ fix: keep OIDC refresh bridge during recovery grace

After successful bridged refresh recovery, re-store the stale-cookie bridge
with a short grace TTL instead of deleting it immediately. This lets parallel
/api/auth/refresh requests that already sent the stale browser cookie recover
before they can observe the first response's Set-Cookie.

Retarget the bridge to the refresh token returned by the bridged retry so
B-to-C refresh-token rotation remains recoverable. The grace TTL is parsed with
math() and defaults to 60s, which shrinks the replay window from the original
REFRESH_TOKEN_EXPIRY bridge lifetime to the short recovery grace period.

Remove the now-unused explicit bridge delete path from the service and
data-schemas method surface. Add regression coverage for grace re-store,
identity symmetry, retry failure behavior, and same-key upsert replacement.

* 🛠️ fix: fail closed on OBO MCP user identity mismatch

Add an OBO-specific guard before MCP tool execution that requires the
effective invocation user and captured request user to both have ids and
to match. This prevents OBO tool calls from falling back to a separate
configurable.user_id identity after request-bound OBO context has already
been captured.

Keep the existing user id fallback behavior for non-OBO MCP calls.

Tests cover mismatched OBO users, missing user ids, and the matching-user
path ignoring a conflicting configurable.user_id.

* 🛠️ fix: Guard OpenID bridge retry user identity

Extract the shared OpenID refresh/user-resolution flow in AuthController
so the normal refresh path and bridge-recovery retry use the same grant,
claims, issuer, user lookup, and diagnostic logging code.

Preserve the existing path-specific behavior: the normal path still owns
migration updates and 401 login redirects, while the bridge retry still
falls through to the existing 403 invalid-token response.

Add a bridge-recovery guard that rejects retry results whose resolved
user id differs from the signed openid_user_id cookie before issuing
tokens or re-storing the grace bridge. Cover both the successful
matching-user recovery and the mismatched-user rejection.

* 🛠️ fix: type-safety polish on OBO data layer

Replace refresh token bridge query/update Record<string, unknown> usage
with typed Mongoose FilterQuery and UpdateQuery definitions.

Harden OpenID marker cookie JWT expiry handling by converting refresh
expiry milliseconds to integer seconds and rejecting invalid or
non-positive durations.

Add focused CSRF tests for fractional refresh expiry values and invalid
expiry configuration.

* 🛠️ fix: Bind OpenID session tokens to authenticated identity

Stamp OpenID session token state with the LibreChat user id, OpenID subject,
tenant id, and normalized issuer when tokens are stored.

Fail closed before OBO inline token reuse/refresh when the session token
identity does not match the current authenticated identity, preventing a stale
or mixed Express session from supplying another user's upstream assertion.

Also validate the normal /api/auth/refresh session-token reuse shortcut against
the signed marker-cookie user before returning cached session tokens.

Note: sessions created before this change carry no identity stamp and are
treated as a mismatch. This is self-healing — the reuse path forces a full IdP
refresh (which re-stamps the session) and the OBO path throws, surfacing as a
one-time re-authentication for active OBO users at deploy time. The session
re-stamps within one session lifetime (SESSION_EXPIRY, default 15 min).

* 🛠️ fix: Recover OpenID refresh token drift

Prefer the browser refresh-token cookie when it differs from the
server-side OpenID session state, and force a real IdP refresh in that
case instead of reusing stale session tokens.

Store a short-lived refresh-token bridge when inline OBO refresh writes
a rotated browser cookie but session persistence fails, so follow-up
refreshes can still recover from the old token.

Keep the bridge grace TTL centralized in RefreshTokenBridge so both
recovery paths use the same env-backed value.

Note: drift is measured against the last-synced browserRefreshToken
marker, so the SSE path (intentionally stale cookie, authoritative
session) does not false-positive. Sessions predating the marker have no
browserRefreshToken; for those, drift falls back to comparing the cookie
against the session refresh token and prefers the cookie on difference.
This is the same self-healing pre-change-session window as the identity
binding fix and re-syncs within one session lifetime.

Tests cover cookie/session drift selection, reusable-session bypass on
drift, bridge storage after session-save failure, and the shared bridge
constant wiring.

* 🛠️ fix: Harden OBO token caching and expiry handling

Reject malformed OBO grant responses before writing them to the exchanged-token cache so a missing access_token cannot poison the cache.

Store absolute expires_at values with cached OBO tokens and ignore legacy cache entries without usable expiry metadata. This keeps cached-token freshness based on the token’s real
remaining lifetime instead of reusing the original relative expires_in on cache hits.

Move OBO expiry normalization and skew helpers into packages/api and use them from both the JS exchange service and the TS MCP resolver. Apply a 30-second safety margin with a one-
second floor for short-lived tokens, covered by direct helper tests and caller-level regression tests.

Tests:
- packages/api: npm run build
- packages/api: npx jest src/mcp/oauth/expiry.spec.ts src/mcp/oauth/obo.spec.ts
- api: npx jest server/services/OboTokenService.spec.js

* 🛠️ fix: Harden OBO refresh-token bridge lookup and indexing

Reuse getValidOpenIDReuseUserId for the bridge-recovery user lookup in
refreshController instead of re-verifying openid_user_id inline. The shared
helper enforces the JWT_REFRESH_SECRET presence check and a strict
typeof payload.id === 'string' guard, rejecting tokens whose id claim is
present but not a string (e.g. a numeric id) that the inline check accepted.

Fail closed on issuer mismatch in getRefreshTokenBridge. Both the stored and
the expected issuer are now normalized and compared for equality, so a bridge
is recovered only when both sides agree (both absent, or both present and
equal after normalization). Previously the check was skipped whenever the
stored issuer was absent, allowing recovery across mismatched issuer context.

Drop the unused {oldRefreshTokenHash, userId, tenantId, openidIssuer} index
and the openidIssuer field on RefreshTokenBridgeQuery. The data-layer filter
only queries the 3-field {oldRefreshTokenHash, userId, tenantId} index; the
issuer is verified in application code, not the query. Hoist the repeated
model accessor into getRefreshTokenBridgeModel.

Note: issuer is now load-bearing for recovery. A bridge stored with an issuer
recovers only when the lookup supplies a matching issuer; the recovery lookup
reads user.openidIssuer via AUTH_REFRESH_USER_PROJECTION (an exclusion
projection that retains the field). If a user's persisted openidIssuer is
empty while the stored bridge has one, recovery fails closed (falls through to
normal re-authentication) until the bridge TTLs out — no security regression.

Tests cover invalid signed-cookie payloads bypassing the bridge, both
asymmetric issuer-presence cases, issuer normalization before comparison, and
an index-alignment assertion guarding against re-adding the dropped index.

* 🛠️ fix: Degrade OBO discovery on token resolution failures

Catch expected OboTokenResolutionError failures during MCP tool discovery and
fall back to unauthenticated tool listing instead of aborting discovery. This
keeps discovery aligned with the existing unauthenticated listing behavior while
preserving unexpected errors as real failures.

Also correct OBO tool-call freshness comment and tighten the OBO trust-check
permissions type to the existing role permission shape.

Tests:
- npx jest src/mcp/__tests__/MCPConnectionFactory.test.ts --runInBand --coverage=false
- npx jest src/mcp/oauth/obo.spec.ts --runInBand --coverage=false

* 🛠️ fix: tighten OBO tool-call errors, bridge logging, and flight typing

Move resolveToolCallUserId inside the tool-call try/catch so an OBO
identity mismatch surfaces with serverName/toolName context and the
standard tool-call-failed message instead of an opaque bare Error.

Raise the refresh-token bridge lookup failure log from debug to warn so
transient infrastructure failures on the unauthenticated /api/auth/refresh
path are observable, and guard the message access against non-Error values.

Replace the unknown+cast in isDuplicateKeyError with a hasErrorCode type
predicate so the duplicate-key check reads error.code without an assertion.

Preserve real math/isEnabled in the MCPConnectionFactory test mock (mock
only processMCPEnv) so mcpConfig timeouts no longer resolve to NaN, fixing
the TimeoutNaNWarning that masked slow OAuth retry behavior.

* 🧪 fix: Restore the Flight Uniqueness Index and Buffer the Graph Cache TTL

Two CI failures on the merge, both in suites this environment cannot run
(their MongoDB binary download is blocked).

`GraphApiService.spec.js` still asserted the unbuffered TTL. Graph tokens
route through the same `getTokenCacheTtlMs` as the OBO and openidStrategy
caches, so the entry now expires 30s before the credential does.

`openidRefreshFlight.spec.ts` dropped the database between tests, which takes
the indexes with it, and Mongoose builds them only once when the model is
compiled. Whether the unique `key` index survived into a test was a race with
that one-time build. Without it a second `create` inserts instead of raising a
duplicate-key error, so every worker believes it won the flight — the
mutual exclusion the file exists to prove. Indexes are now rebuilt after each
drop, which also makes the reclaim and complete cases reach those paths for
the right reason.

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

* fix: address OBO review findings

* 🔐 fix: Install Bridge Indexes and Carry OBO Through Assistant Recovery

Two findings from the Codex pass on d08c82f0d.

The refresh-token bridge relied on Mongoose auto-indexing for both of its
indexes, and `MONGO_AUTO_INDEX=false` is a supported deployment setting. A
bridge holds an encrypted refresh token and the TTL index is the only thing
that ever deletes one, so under that setting they would accumulate for the
life of the collection while concurrent upserts lost the compound uniqueness
the filter assumes. Installed before the first write, matching the flight
methods and the session and schedule methods before them.

`recoverServerTools`, the assistant create/update path that reruns
`reinitMCPServer` when a referenced server's catalog and connection snapshot
are both missing, was the last reinit site not carrying the upstream-token
closure. For an OBO server the factory rejects the connection outright, so the
assistant write failed with unavailable MCP definitions. It now builds the
provider at that request boundary like the other entry points; assistant
writes have no `res`, so a rotation there falls back to the recovery bridge.

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

* fix: harden OBO refresh coordination

* ⏳ fix: Keep Elapsed Expiries Elapsed and Revoke the Superseded Session

Two of the five findings from the Codex pass on fcdc15885 — the two that are
defects in code this branch introduced rather than design questions about the
bridge.

`getSkewedTokenExpiresAtMs` floored every result at a second in the future,
including an expiry the provider had already declared elapsed. An exchange
answering `expires_in: 0` or a past `expires_at` was handed to the MCP
connection stamped valid for another second, which only moves the failure
downstream. The floor now applies to a lifetime that is still live, which is
what it was for; an elapsed one stays elapsed so the caller rejects it. Same
for the cache TTL, which falls back to the elapsed-credential floor.

Bridge recovery left the stale token's durable Session behind. Only the token
it recovered through was passed as `existingRefreshToken`, so that one's
session was replaced while the token the browser actually presented kept its
record until its original expiry. That record, with the marker cookie still
bound to it, is what authorizes local image access for OpenID users — so a
copy of the stale cookie outlived the rotation it had lost. Revoked
explicitly on successful recovery.

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

* fix: close OBO refresh review findings

* 🎟️ fix: Carry the Bridged Token Through a Non-Rotating Recovery

Bridge recovery passes the browser's stale token as `existingRefreshToken` so
the durable Session naming it is the record replaced. That also makes the
stale token the fallback the installed session and the refresh cookie use when
a tokenset carries no `refresh_token` of its own, which holds only while the
recovery grant rotates.

An IdP that answers that grant without rotating sends the browser back to the
very token the bridge exists to retire: `storeOpenIDSession` installs and
deletes the same stale record in one call, and the cookie is rewritten to a
token the IdP already rejected — a sign-out on the next refresh. The grace
bridge one line above already guards this with `|| bridgedRefreshToken`; the
resolved tokenset now does the same, so leader and followers alike publish the
recovered token.

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

* 🚫 fix: Reject an OBO Exchange That Returns an Expired Credential

Preserving an elapsed expiry through the skew helper only helps if something
acts on it, and nothing did: `MCPManager.callTool` checks the access token and
nothing else before setting the Authorization header, so a credential the IdP
declared spent still went downstream to fail there. It is rejected at the
exchange now, where the reason is known, and retryably — the exchange itself
worked, so a fresh grant can succeed.

Completes the elapsed-expiry change in cb26a6f7d, which made the stamp honest
without giving anyone a reason to look at it.

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

* fix: close OpenID refresh review findings

* fix: coordinate OpenID refresh entry points

* chore: sort OpenID flight imports

* fix: fence OpenID refreshes during logout

* fix: close OpenID logout publication races

* fix: narrow completed refresh flight

* test: cover bridge cleanup failure after ownership loss

The compensating delete in storeRefreshTokenBridgeWithLease swallows its own
failure so the lease error stays the one the caller sees. Nothing asserted
that, so removing the inner catch left every suite green while callers began
receiving the cleanup error instead.

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

* fix: compensate OpenID bridges only on proven ownership loss

The post-write lease assertion deletes the bridge it just published when it
throws, but it threw for two different reasons: a coordination record that is
no longer ours, and a coordination read that simply failed. Treating the second
as the first destroys the only mapping from the token the browser still holds
to the one the IdP already rotated to, so a transient Mongo error on the
headers-already-sent path signed the user out.

Tag the ownership error where the lease raises it and compensate only for that,
preserving the bridge whenever ownership is merely undetermined. A preserved
bridge stays behind the logout revocation fence, so the safe default costs
nothing.

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

* fix: reject spent OpenID refresh results

Two ways a refresh could report success while handing back a credential
nothing can use.

An inline refresh carries the previous id_token forward when the IdP omits
one on rotation, so tokenset.id_token is not necessarily freshly issued.
setOpenIDAuthTokens applied its freshness guard only to the session copy and
took tokenset.id_token unconditionally, so /refresh returned an expired
bearer even though the grant produced a usable access token. Skip it only
when it is provably expired: an id_token whose expiry cannot be read stays
preferred, since access_token may be opaque or scoped to another audience.

normalizeExpiresIn preserves a zero or negative lifetime rather than
discarding it, so a grant declaring an already-spent access token still
published, rotating the refresh token and returning a token every freshness
check rejects. Each OBO call then repeated the grant. Reject an elapsed
lifetime before publishing; an unknown lifetime still publishes.

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

* fix: harden OpenID refresh publication

* fix: keep identity and results intact through OpenID refresh cleanup

Two follow-ons from the last round's fixes.

Stripping an expired carried-forward id_token from the refresh result removed
the only identity material a rotation without id_token leaves behind. The
result is rebuilt by buildOIDCTokensFromSession, so it carries no provider
claims() either, and getTokenClaims accepts only those two — bridge recovery
failed with "no usable identity claims" before setOpenIDAuthTokens could hand
back the fresh access token. The stripped token now travels in a
non-enumerable marker, alongside the existing browser and predecessor markers,
which identity resolution reads and the authentication response never sees.

The lease drained a pending renewal by awaiting it in finally, so a transient
coordination failure there threw from finally and replaced the operation's
result. The refresh had already settled and published, so the caller saw a
failure on credentials that had rotated. Proven ownership loss is recorded on
ownershipLost and checked before the return, so the drain has nothing to add
but noise; it now absorbs and logs.

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

* refactor: fence OpenID recovery publication

* fix: close OpenID publication transaction

* refactor: make OpenID publication transactional

* fix: satisfy OpenID publication type checks

* fix: fence OpenID session publication

* fix: authorize OpenID refresh publication

* fix: bind OpenID replay generations

* fix: fence OpenID response generations

* fix: authorize OpenID token delivery

* fix: linearize OpenID publication delivery

* chore: sort OpenID refresh flight imports

---------

Co-authored-by: J.C. Bartle <jcbartle@users.noreply.github.com>
Co-authored-by: jbartle <jbartle@rand.org>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jcbartle <7274202+jcbartle@users.noreply.github.com>
2026-08-29 23:55:58 -04:00
Danny Avila
10981b8637
🍱 feat: Guide Compaction With a Bounded Semantic Index (#15340)
* 🧭 feat: guide compaction with semantic context

* 🛡️ fix: fail closed on semantic intent collisions
2026-08-29 17:53:04 -04:00