Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
02492bd
🏚️ feat: Recover Coding Chats With Missing Workspaces (#16273)
lia-by-librechat[bot] Sep 24, 2026
3a3fbb7
🏝️ ci: Keep Canary Pull Requests on Canary (#16285)
lia-by-librechat[bot] Sep 24, 2026
6a58bf1
🎈 fix: Restore Bedrock GPT Context for Long Prompts (#16284)
lia-by-librechat[bot] Sep 24, 2026
290e37e
🐤 docs: Clarify Maintainer-Directed Canary Pull Requests (#16291)
lia-by-librechat[bot] Sep 24, 2026
8fb4056
🦺 fix: Skip Redundant MCP OAuth Probes and Teardown Cancellations (#1…
lia-by-librechat[bot] Sep 24, 2026
d0850e7
🪁 fix: Keep Workspace Discovery Reactive (#16296)
lia-by-librechat[bot] Sep 24, 2026
7d5cbc9
🌫️ feat: Add Bounded PII Text Transformation (#16295)
lia-by-librechat[bot] Sep 24, 2026
e6929f5
🪴 fix: Surface Persistent Memory Before First Save (#16301)
lia-by-librechat[bot] Sep 24, 2026
ffd3d1e
🔕 perf: Back Off Idle Durable Agent Recovery Polls (#16303)
lia-by-librechat[bot] Sep 24, 2026
0829709
🥞 fix: Keep Tool Glyphs Behind Approval Reviews (#16307)
lia-by-librechat[bot] Sep 24, 2026
04e637e
🦉 fix: Quiet Idle Subagent Discovery While Preserving Child Wakeups (…
lia-by-librechat[bot] Sep 24, 2026
56301c1
🪚 fix: Count Large Prompts in Bounded Tokenizer Chunks (#16305)
lia-by-librechat[bot] Sep 24, 2026
edddf92
🪀 fix: Preserve Coding Approval Mode on Background Returns (#16322)
lia-by-librechat[bot] Sep 24, 2026
3788fe0
🐢 fix: Back Off Waiting Completion Wake-ups and Keep Their Trace Out …
danny-avila Sep 25, 2026
402c19c
🧪 ci: Count System Instructions in Mock Model Usage (#16350)
danny-avila Sep 25, 2026
7238be3
⚡ fix: Deliver Waiting Completion Wake-ups the Moment They Are Ready …
danny-avila Sep 25, 2026
5862b3b
🛍️ fix: Discover Managed Agents in Marketplace Search (#16324)
lia-by-librechat[bot] Sep 25, 2026
dd21e7f
📣 fix: Surface MCP Credential Failures During Tool Loading (#16323)
lia-by-librechat[bot] Sep 25, 2026
c6282fa
🧷 fix: Wait for Long Dispatch Turns Before Giving Up Background Code …
danny-avila Sep 25, 2026
34938eb
📮 fix: Count Undelivered Background Results as Outstanding (#16343)
danny-avila Sep 25, 2026
fe8a993
🚥 fix: Pause Rejected Steer Recoveries (#16306)
lia-by-librechat[bot] Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/scripts/retarget-prs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ for number in "$@"; do
continue
fi

# Canary is an intentional integration target. Keep it untouched even if a caller passes
# its number to the sweep or overrides RELEASE_BASE, TARGET_BASE or EXPLAIN_MISSING.
if [ "$base_ref" = "canary" ]; then
echo "#$number: skipped — canary is an explicit target"
skipped=$((skipped + 1))
continue
fi

if [ "$base_ref" = "$TARGET_BASE" ] && [ "$EXPLAIN_MISSING" = "true" ] && [ "$DRY_RUN" != "true" ]; then
echo "#$number: already on $TARGET_BASE — posting any missing explanation"
post_explanation "$number"
Expand Down
104 changes: 104 additions & 0 deletions .github/scripts/retarget-prs.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';

const scriptsDir = dirname(fileURLToPath(import.meta.url));
const script = join(scriptsDir, 'retarget-prs.sh');

function runRetarget(t, prs, overrides = {}) {
const scratch = mkdtempSync(join(scriptsDir, '.retarget-prs-test-'));
t.after(() => rmSync(scratch, { recursive: true, force: true }));
const callsPath = join(scratch, 'calls');
const fakeGh = join(scratch, 'gh');
writeFileSync(
fakeGh,
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "$CALLS_PATH"
case "$1:$2" in
api:*/pulls/*) cat "$FIXTURE_DIR/\${2##*/}.json" ;;
api:*/comments) printf '%s\\n' '[]' ;;
'pr:edit') exit 0 ;;
'pr:comment') cat >/dev/null ;;
*) echo "unexpected gh call: $*" >&2; exit 1 ;;
esac
`,
{ mode: 0o700 },
);
for (const [number, { base, head = 'topic' }] of Object.entries(prs)) {
writeFileSync(
join(scratch, `${number}.json`),
JSON.stringify({
state: 'open',
base: { ref: base },
head: { ref: head, repo: { full_name: 'contributor/fork' } },
labels: [],
}),
);
}
const result = spawnSync('bash', [script, ...Object.keys(prs)], {
encoding: 'utf8',
env: {
...process.env,
PATH: `${scratch}:${process.env.PATH}`,
FIXTURE_DIR: scratch,
CALLS_PATH: callsPath,
REPO: 'LibreChat-AI/LibreChat',
RELEASE_BASE: 'main',
TARGET_BASE: 'dev',
DRY_RUN: 'false',
EXPLAIN_MISSING: 'false',
...overrides,
},
});
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
return {
output: result.stdout,
calls: readFileSync(callsPath, 'utf8').trim().split('\n'),
};
}

function writes(calls) {
return calls.filter((call) => call.startsWith('pr edit ') || call.startsWith('pr comment '));
}

test('a numbered manual sweep leaves canary alone while main still retargets to dev', (t) => {
const { output, calls } = runRetarget(t, {
101: { base: 'canary' },
102: { base: 'main' },
});
assert.match(output, /#101: skipped/);
assert.deepEqual(writes(calls), [
'pr edit 102 --repo LibreChat-AI/LibreChat --base dev',
'pr comment 102 --repo LibreChat-AI/LibreChat --body-file -',
]);
});

test('canary stays protected even if a caller misconfigures the release base', (t) => {
const { output, calls } = runRetarget(t, { 103: { base: 'canary' } }, { RELEASE_BASE: 'canary' });
assert.match(output, /#103: skipped/);
assert.deepEqual(writes(calls), []);
});

test('explanation recovery never posts a dev-target message on a canary PR', (t) => {
const { output, calls } = runRetarget(
t,
{ 104: { base: 'canary' } },
{ TARGET_BASE: 'canary', EXPLAIN_MISSING: 'true' },
);
assert.match(output, /#104: skipped/);
assert.deepEqual(writes(calls), []);
});

test('a dry run cannot propose retargeting canary', (t) => {
const { output, calls } = runRetarget(
t,
{ 105: { base: 'canary' } },
{ RELEASE_BASE: 'canary', DRY_RUN: 'true' },
);
assert.match(output, /would_retarget=0 skipped=1/);
assert.deepEqual(writes(calls), []);
});
9 changes: 8 additions & 1 deletion .github/workflows/pr-retarget-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Retarget PRs to dev
on:
pull_request_target:
types: [opened, reopened, synchronize]
# A deliberate canary base is never retargeted; the script also enforces this for sweeps.
branches: [main]
workflow_dispatch:
inputs:
Expand All @@ -11,7 +12,7 @@ on:
type: boolean
default: true
pr_numbers:
description: 'Space-separated PR numbers (default: every open pull request based on main)'
description: 'Space-separated PR numbers (default: open main-based PRs; canary is excluded)'
required: false
default: ''

Expand Down Expand Up @@ -41,6 +42,9 @@ jobs:
persist-credentials: false
sparse-checkout: .github/scripts

- name: Verify canary base stays untouched
run: node --test .github/scripts/retarget-prs.test.mjs

- name: Retarget onto dev
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -59,6 +63,9 @@ jobs:
persist-credentials: false
sparse-checkout: .github/scripts

- name: Verify canary base stays untouched
run: node --test .github/scripts/retarget-prs.test.mjs

- name: Retarget onto dev
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
25 changes: 18 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@ See CLAUDE.md.

## Branching and pull requests

Branch off `dev` and target `dev` with every pull request; `gh pr create` defaults to `main`, so
pass `--base dev` explicitly. `main` is the released branch, kept as a fast-forward of `dev` and
synced as-is — never open a backport pull request to `main`, because anything merged to `dev`
reaches it at the next sync. Pull requests opened against `main` are retargeted automatically.
`Fixes #N` does not close the issue on a `dev` merge — GitHub honors closing keywords only on the
default branch, so close linked issues by hand. Worktrees share one stash stack, so never use a bare
`git stash pop`. See the detailed policy in `CLAUDE.md` under "Branching and Pull Requests".
Normally branch off `dev` and target `dev`; `gh pr create` defaults to `main`, so pass `--base dev`
explicitly. `main` is the released branch, kept as a fast-forward of `dev` and synced as-is — never
open a backport pull request to `main`, because anything merged to `dev` reaches it at the next
sync. Pull requests opened against `main` are retargeted automatically.

**Maintainer-directed canary exception:** Experimental work, or a PR ready to merge after review but
not yet suitable for the next `main` sync, may instead target `canary`. The maintainer may choose
this before work starts or while reviewing a stale PR. For new canary work, branch from the current
`origin/canary` and pass `--base canary` explicitly; do not silently retarget an existing PR or
promote canary code to `dev`. If a stale PR is redirected to canary, first check its base, diff and
reviewed head against current canary; coordinate any rebase or new PR with the maintainer. A PR
explicitly based on `canary` stays there; the main-to-dev retarget workflow does not move it.

Still link related issues in the PR (for example, `Related to #N`) so the work remains
traceable. `Fixes #N` does not close an issue on a `dev` or `canary` merge — GitHub honors closing
keywords only on the default branch. Close resolved issues by hand after merging. Worktrees share
one stash stack, so never use a bare `git stash pop`. See the detailed policy in `CLAUDE.md` under
"Branching and Pull Requests".

Write the description for a reader who has not followed the branch: what breaks, what triggers it,
how it behaves after the change, then one or two views of the mechanism — a focused diff, a call
Expand Down
29 changes: 22 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,36 @@ The source code for `@librechat/agents` (major backend dependency, same team) li

## Branching and Pull Requests

- **Branch off `dev`, and target `dev` with every pull request.** All work lands on `dev` first.
- **Normally branch off `dev` and target `dev`.** This is the default for work ready to follow the
regular release path. A maintainer-directed canary exception is described below.
- **`main` is the released branch.** It is kept as a fast-forward of `dev` and synced as-is, so it
is always an ancestor of `dev` — equal to it right after a sync, behind it otherwise. It never
carries a commit that `dev` does not have.
- **Never open a backport pull request to `main`.** Anything merged to `dev` reaches `main` at the
next sync; a second pull request for the same change is redundant.
- **The repository's default branch is `main`**, so `gh pr create` and the GitHub UI target it
unless told otherwise — always pass `--base dev` explicitly.
unless told otherwise — pass `--base dev` for normal work or `--base canary` for an explicitly
maintainer-directed canary PR.
- Pull requests opened against `main` are retargeted to `dev` automatically by
`.github/workflows/pr-retarget-dev.yml`. The `target: main` label exempts one, as do release-bound
upstream branches (`dev`, `release/*`, `hotfix/*`). Backport branches are deliberately not exempt —
a backport merged straight to `main` is what breaks the fast-forward invariant.
- **`Fixes #N` does not close the issue.** GitHub honors closing keywords only when a pull request
merges into the default branch (`main`). Merging to `dev` does not close anything, and the later
fast-forward of `main` is not a merge event either — close linked issues by hand.
upstream branches (`dev`, `release/*`, `hotfix/*`). PRs deliberately based on `canary` are not
retargeted, including manual sweeps; the script skips them explicitly. Backport branches are
deliberately not exempt — a backport merged straight to `main` breaks the fast-forward invariant.
- **Canary is an explicit, maintainer-selected integration target, not a path that automatically
flows to `dev` or `main`.** Experimental changes and PRs that are ready to merge following review
but must not enter the next `main` sync can go to `canary`. The maintainer may decide this when
assigning the work or while reviewing an older/stale PR. For a new canary PR, branch from the
current `origin/canary` and set `--base canary`. Never infer that a canary-targeted PR should move
to `dev` just because `dev` is the default; do not copy canary features into `dev` without an
explicit maintainer decision. If a stale PR is reassigned to canary, check the PR's base, diff and
exact reviewed head against current canary before changing its base or proposing a new canary
branch/PR; if a rebase changes the head, verify and review that new head before merging.
- **Link related issues in the PR even when they will not auto-close.** Reference each relevant
issue in the description (for example, `Related to #N`) so reviewers can find the context and
track the work. GitHub honors `Fixes #N` and other closing keywords only when a pull request
merges into the default branch (`main`). Merging to `dev` or `canary` does not close an issue,
and the later fast-forward of `main` is not a merge event either — close resolved issues by hand
after merging.
- **Git worktrees share one stash stack.** `refs/stash` lives in the common `.git` directory, so a
bare `git stash pop` in one worktree can take work stashed in another. Prefer a throwaway WIP
commit; if you must stash, `git stash push -m <tag>` and `apply` that specific entry.
Expand Down
109 changes: 28 additions & 81 deletions api/app/clients/tools/util/handleTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
toolRolePermissions,
checkToolRolePermission,
createSafeUser,
loadMCPTools,
createAuthIdentityContext,
selectMCPUpstreamTokenProvider,
mcpToolPattern,
Expand Down Expand Up @@ -621,10 +622,6 @@ const loadTools = async ({
}

const loadedTools = (await Promise.all(toolPromises)).flatMap((plugin) => plugin || []);
const mcpToolPromises = [];
/** MCP server tools are initialized sequentially by server */
let index = -1;
const failedMCPServers = new Set();
const safeUser = createSafeUser(options.req?.user);
const requestScopedConnections =
options.requestScopedConnections ?? getMCPRequestContext(options.req, options.res);
Expand Down Expand Up @@ -653,83 +650,33 @@ const loadTools = async ({
}),
});

for (const [serverName, toolConfigs] of Object.entries(requestedMCPTools)) {
index++;
/** @type {LCAvailableTools} */
let availableTools = options.mcpAvailableTools?.[serverName];
for (const config of toolConfigs) {
try {
if (failedMCPServers.has(serverName)) {
continue;
}
const mcpParams = {
mcpPermissionContext,
index,
signal,
user: safeUser,
userMCPAuthMap,
configServers,
requestBody: options.requestBody ?? options.req?.body,
requestScopedConnections,
res: options.res,
upstreamTokenProvider,
upstreamTokenProviderResolver,
oboIdentityContext,
streamId: options.req?._resumableStreamId || null,
jobCreatedAt: options.jobCreatedAt,
model: agent?.model ?? model,
serverName: config.serverName,
provider: agent?.provider ?? endpoint,
config: config.config,
};

if (config.type === 'all' && toolConfigs.length === 1) {
/** Handle async loading for single 'all' tool config */
mcpToolPromises.push(
createMCPTools(mcpParams).catch((error) => {
logger.error(`Error loading ${serverName} tools:`, error);
return null;
}),
);
continue;
}
if (!availableTools) {
try {
availableTools = await getMCPServerTools(safeUser.id, serverName, config.config);
} catch (error) {
logger.error(`Error fetching available tools for MCP server ${serverName}:`, error);
}
}

/** Handle synchronous loading */
const mcpTool =
config.type === 'all'
? await createMCPTools(mcpParams)
: await createMCPTool({
...mcpParams,
availableTools,
toolKey: config.toolKey,
onAvailableTools: (tools) => {
availableTools = tools;
},
});

if (Array.isArray(mcpTool)) {
loadedTools.push(...mcpTool);
} else if (mcpTool) {
loadedTools.push(mcpTool);
} else {
failedMCPServers.add(serverName);
logger.warn(
`MCP tool creation failed for "${config.toolKey}", server may be unavailable or unauthenticated.`,
);
}
} catch (error) {
logger.error(`Error loading MCP tool for server ${serverName}:`, error);
}
}
}
loadedTools.push(...(await Promise.all(mcpToolPromises)).flatMap((plugin) => plugin || []));
loadedTools.push(
...(await loadMCPTools({
userId: user,
requestedTools: requestedMCPTools,
availableTools: options.mcpAvailableTools,
createTools: createMCPTools,
createTool: createMCPTool,
getAvailableTools: getMCPServerTools,
context: {
mcpPermissionContext,
signal,
user: safeUser,
userMCPAuthMap,
configServers,
requestBody: options.requestBody ?? options.req?.body,
requestScopedConnections,
res: options.res,
upstreamTokenProvider,
upstreamTokenProviderResolver,
oboIdentityContext,
streamId: options.req?._resumableStreamId || null,
jobCreatedAt: options.jobCreatedAt,
model: agent?.model ?? model,
provider: agent?.provider ?? endpoint,
},
})),
);
return { loadedTools, toolContextMap, dynamicToolContextMap, primedCodeFiles };
};

Expand Down
Loading
Loading