LibreChat/api/server/middleware
Danny Avila 0219513115
🛑 fix: Forward Run Abort Signal to Foreground Tool Calls (#15517)
* 🛑 fix: Forward Run Abort Signal to Foreground Tool Calls

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

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

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

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

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

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

Addresses both findings from the Codex review of 48fded564a.

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

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

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

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

Addresses both findings from the Codex review of b3c67082cd.

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

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

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

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

Addresses the Codex review of d53ff170ab.

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

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

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

* 🪪 fix: Keep an Action OAuth Abort an Abort

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

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

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

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

Addresses the Codex review of 9bcf1aa57e.

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

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

* 🧹 chore: Restore Import Order in the Agents Tool Handler
2026-09-02 13:32:33 -04:00
..
__tests__ 🌐 fix: Expose Gemini Models to Vertex AI Agents (#15234) 2026-08-27 06:50:23 -04:00
accessResources 🖇️ fix: Bind Model-Spec Authorization to the Loaded Agent (#15256) 2026-08-26 10:38:20 -04:00
assistants
config
limiters 🛫 feat: Enable Self-Service Code Worker Pairing (#15367) 2026-09-01 18:46:33 -04:00
roles 🪢 feat: show Langfuse session link in shared chats (#15273) 2026-08-27 14:29:57 -04:00
spec 🎟️ refactor: Require Credentials for Local Image Access by Default (#15252) 2026-08-27 09:55:27 -04:00
validate ⚡ perf: Cut Serial Round Trips and a 100ms Admission Stall from Chat Turns (#15138) 2026-08-23 15:17:35 -04:00
abortMiddleware.js 🛑 fix: Forward Run Abort Signal to Foreground Tool Calls (#15517) 2026-09-02 13:32:33 -04:00
abortMiddleware.spec.js 🛑 fix: Forward Run Abort Signal to Foreground Tool Calls (#15517) 2026-09-02 13:32:33 -04:00
abortRun.js 🧾 fix: Honor Disabled Transactions on the Assistants Usage Path (#15100) 2026-08-25 08:30:12 -04:00
buildEndpointOption.js 🖇️ fix: Bind Model-Spec Authorization to the Loaded Agent (#15256) 2026-08-26 10:38:20 -04:00
buildEndpointOption.spec.js 🛂 feat: Filter Model-Bound Content by Source (#14425) 2026-08-21 22:43:32 -04:00
canAccessSharedLink.js
canDeleteAccount.js
canDeleteAccount.spec.js
checkBan.js 🔐 fix: Redirect OAuth Browser Navigations Instead of Rendering JSON (#15440) 2026-08-31 21:13:47 -04:00
checkDomainAllowed.js
checkInviteUser.js
checkPeoplePickerAccess.js
checkPeoplePickerAccess.spec.js
checkSharePublicAccess.js
checkSharePublicAccess.spec.js
denyRequest.js
error.js
index.js 🔐 fix: Redirect OAuth Browser Navigations Instead of Rendering JSON (#15440) 2026-08-31 21:13:47 -04:00
logHeaders.js
messageValidation.js ⚡ perf: Optimize First Load of Large Conversations (#14901) 2026-08-16 19:45:21 -04:00
moderateText.js 🧢 fix: Cap Batched HITL Answers Before PII and Moderation (#15257) 2026-08-26 10:10:19 -04:00
moderateText.spec.js 🧢 fix: Cap Batched HITL Answers Before PII and Moderation (#15257) 2026-08-26 10:10:19 -04:00
noIndex.js
oauthNavigation.js 🔐 fix: Redirect OAuth Browser Navigations Instead of Rendering JSON (#15440) 2026-08-31 21:13:47 -04:00
optionalJwtAuth.js
optionalShareFileAuth.js
optionalShareFileAuth.spec.js
requireJwtAuth.js 📬 feat: Add Durable Agent Trigger Delivery (#14925) 2026-08-17 09:25:08 -04:00
requireLdapAuth.js
requireLocalAuth.js
setHeaders.js
setTwoFactorTempUser.js
uaParser.js
validateEmailLogin.js
validateImageRequest.js 🎟️ refactor: Require Credentials for Local Image Access by Default (#15252) 2026-08-27 09:55:27 -04:00
validateMessageReq.js
validateModel.js 🌐 fix: Expose Gemini Models to Vertex AI Agents (#15234) 2026-08-27 06:50:23 -04:00
validatePasswordReset.js
validateRegistration.js