LibreChat/api/server/services
lia-by-librechat[bot] 7b2362d7a7
Some checks failed
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Backend Unit Tests / Build packages (push) Has been cancelled
Backend Unit Tests / Codegraph select (push) Has been cancelled
Backend Unit Tests / Circular dependency checks (push) Has been cancelled
Frontend Unit Tests / Client build recovery regression (push) Has been cancelled
Frontend Unit Tests / Codegraph select (push) Has been cancelled
Frontend Unit Tests / Build packages (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Docker Dev Images Build / publish (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
Backend Unit Tests / TypeScript type checks (push) Has been cancelled
Backend Unit Tests / Tests: api (shard 1/3) (push) Has been cancelled
Backend Unit Tests / Tests: api (shard 2/3) (push) Has been cancelled
Backend Unit Tests / Tests: api (shard 3/3) (push) Has been cancelled
Backend Unit Tests / Tests: data-provider (push) Has been cancelled
Backend Unit Tests / Tests: data-schemas (push) Has been cancelled
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Has been cancelled
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Has been cancelled
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Has been cancelled
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Has been cancelled
Frontend Unit Tests / TypeScript type checks (client) (push) Has been cancelled
Frontend Unit Tests / Tests: @librechat/client (push) Has been cancelled
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Has been cancelled
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Has been cancelled
Frontend Unit Tests / Vite build verification (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
🧲 feat: Attach or Leave a Workspace on a Saved Chat (#16124)
* feat: Attach or leave a workspace on a saved chat

A saved chat that recorded running without an attached environment could
never take one: `without_attached` is sealed for the life of the chat, and
the only transition was a move between attached environments. Switching an
existing chat to a coding agent therefore ended in a composer that could
send but never reach files or commands. A chat sealed to a machine that went
offline had the opposite dead end: Send stayed disabled with nothing on
offer but a reconnect hint, and the "Move the chat to continue without it"
copy promised a request the server rejected as `invalid`.

One transition mechanism now covers all three directions. `from` still
repeats the persisted decision, so a stale view cannot replace a decision it
has not seen, and only a decision the chat actually recorded can move:

- attach, from a recorded `without_attached` onto the environments its
  agents use, with each target workspace revalidated on its worker;
- detach, an empty `to`, which clears the selections without polling the
  machine the chat is leaving;
- move, unchanged, including its refusal to switch the workspace of an
  environment the decision already covers.

The composer keeps the control visible while a chat runs without a
workspace, so that state is named and reversible, and offers "Continue
without workspace" whenever a sealed machine stops being usable. History is
untouched and no file is copied.

A send now also records the decision it was submitted under on its own
conversation. The server seals that pair for the rest of the chat, so a chat
that became saved mid-run held none while the stored one was already sealed:
the composer re-derived an agent default the seal refuses and reported
"Choose workspace" with Send disabled until the page was reloaded.

* 🎨 style: Format the Workspace Transition Hook Tests

* fix: Never seal a partial workspace decision, and gate the new directions

Two review findings on the transition mechanism.

A transition replaces the sealed decision whole, and the next turn resolves
every environment or none. The move path could not truncate before, because it
was offered only in `choose` state, which `aggregateState` reaches only when no
environment is unavailable, unsupported or missing: every environment was then
either carried over or selectable. Widening the gate to `!canSubmit` so an
unreachable machine could be left behind broke that invariant, and an
environment nothing can cover would have been dropped from a decision the
composer then sealed, blocking the next send and needing a second transition to
escape. A nonempty transition now requires every environment to be retained or
selectable; when one cannot be, only leaving attached execution is offered,
which is the escape.

The move protocol version becomes 2. Attach and detach travel on the same
owner-authorized endpoint as the move, so a client built against this change
would otherwise offer both to a deployment that only implements moves, where an
attach comes back `locked` and an empty target set `invalid`. The client already
compares the advertised number against the constant, so the bump is the gate.
`conversationMoves.enabled` still decides whether an owner may replace a sealed
decision at all.

* fix: Keep v1 moves working, and take the conversation updater from its owner

Two review findings on the previous head.

Bumping the move protocol wholesale made the client's exact-version check treat
a replica still advertising 1 as supporting nothing, so an attached chat whose
agent moved to another machine lost the relocation that replica already served
-- the previous head's own compatibility test pinned that regression. The
protocol is now two constants: version 1 is the move between attached
environments, version 2 adds attaching an environment to a chat that recorded
running without one and leaving attached execution behind. The server
advertises the highest it implements and the client keeps every lower
capability, so a v1 replica keeps its move while attach and detach wait for the
version that serves them. Where an environment cannot be covered and leaving is
not served either, no transition is offered and the composer reports the state.

Recording the submitted decision had made this hook a second Recoil writer of
the conversation atom, widening the surface that migration has to convert. The
host that owns the atom already holds its setter, so the hook receives it
instead of reaching through `~/store`.

* fix: Advertise attach and detach beside the move instead of above it

Raising the move protocol to 2 kept a client and server of the same build in
agreement, but not the two builds a rollout actually runs at once. The deployed
client compares the advertised number for equality against its own constant of
1, so a server advertising 2 tells every client already loaded in a browser that
moves are unsupported, and an attached chat whose agent changed machines loses
its relocation until that tab reloads.

The move version therefore stays 1, exactly as before this branch, and the two
new directions are advertised on their own field:

    codeEnvironmentMoveVersion: 1        // move between attached environments
    codeEnvironmentTransitionVersion: 2  // attach an environment, or leave one

Both come from `conversationMoves.enabled`, since all three are one owner
replacing one sealed decision. A client that predates this capability reads only
the move version and keeps working; a client that has it offers attach and
detach only where the second field appears, so it never sends a replica an
attach it refuses as `locked` or an empty target set it calls `invalid`.

* fix: Hold a send while a workspace decision is being replaced

Confirming an attach left Send enabled. The composer disables only the
workspace menu while the request is in flight, so a turn submitted right after
the confirmation started under the decision being replaced: it ran without the
workspace its owner had just chosen, and the replacement still landed, because
the stored decision the compare-and-swap expects was unchanged. The chat then
reported an environment its last turn never used, and nothing said so.

The server's own check has the same shape. It reads the conversation's
generation state, then revalidates each target workspace over the network, then
swaps; a turn that starts during that round trip is invisible to the check that
already ran. It now re-reads the generation state immediately before the swap,
leaving only the instant the compare-and-swap itself covers, which closes the
window for every client rather than just this one.

On the client the submission resolver refuses while a replacement is pending,
so the same signal disables the Send button and makes a programmatic send
return false. The workspace control stays reachable, so the state a chat is in
remains visible while it settles.

* fix: Fence Workspace Transitions and Reconcile Rejected Choices

Advance a private conversation revision after publishing an active generation and before reading its authoritative workspace decision. Transition CAS writes compare that revision, covering admission during the final idle-check window without expiring locks. Preserve tenant middleware and exclude private counter writes from chat activity and indexing.

Default existing undecided chats to no workspace, reconcile failed submissions from the server, scope pending guards to the affected chat, compare selection tuples without delimiter collisions, and opt attach/detach into deployment policy independently of existing moves. Retain legacy selection-only choices through first-send events.

* fix: Retain Failed Workspace Recovery and Skip Disabled Fences

Keep unconfirmed code-environment decisions in conversation-scoped Jotai state after authoritative reads fail, mutation observers reset, or navigation unmounts them. Expose a retry action and release submission only after a matching successful read. Ignore stale completions and reject mismatched conversation responses.

Reuse the owner-scoped conversation already loaded by each ingress when effective conversation moves are disabled. Retain the admitted revision fence for enabled moves, including move-only deployments. Cover failure/retry lifecycle and both policy branches at the helper and ingress levels.

* fix: Reuse the Atomic Admitted Snapshot Through Recovery

Return the admission revision and persisted decision from one Mongo findOneAndUpdate. Skip content indexing per internal query while retaining tenant middleware, private projection, and timestamps. Carry the returned snapshot through web and API initialization instead of validating against a stale pre-admission selection.

Keep first-send workspace choices in both the recovery preset and conversation template, and reconcile using the generated conversation ID from error or message data rather than requiring a saved submission snapshot. Cover single-query storage, indexing isolation, moved-machine validation, and first-send error recovery.

* fix: Reconcile Cancelled Turns and Keep Detach Reachable

Reconcile the submitted code decision before the local cancellation path finalizes streamed content or recovers a minimal submission. A cancel that beats the server rejection must not leave the attempted choice sealed locally; navigation must not update a different conversation.

Offer the authorized detach-only transition when a sealed conversation no longer has any attached agents, while ordinary chat stays sendable. Keep the selector visible for that transition and retain existing policy gates. Cover cancellation branches, no-attached-agent states, managed-only agents and the detach action.

* fix: Recover Workspace Transitions Across Navigation

* test: Verify Saved Workspace Transitions Against Current Dev

* test: Assert Explicit No-Workspace State in Native Journey

* test: Await Browser Settlement Before Switching Native Agents

* test: Settle URL Agent Selection Before Native Chat

* test: Wait for Worker Lease Expiry Before Offline Detach

---------

Co-authored-by: Lia <lia@librechat.ai>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-25 10:08:28 -04:00
..
__tests__ 🎟️ fix: Preserve BYOM Tool Calls Through Capacity Windows (#16060) 2026-09-19 09:03:06 -04:00
Agents ⚡ fix: Deliver Waiting Completion Wake-ups the Moment They Are Ready (#16339) 2026-09-24 22:28:28 -04:00
Artifacts
Config
Endpoints 🧲 feat: Attach or Leave a Workspace on a Saved Chat (#16124) 2026-09-25 10:08:28 -04:00
Files 🌳 feat: Isolate Attached Workspaces by Conversation (#16107) 2026-09-19 23:19:26 -04:00
MCP
Runs
Schedules
Skills
start
Threads
Tools
ActionService.js
ActionService.spec.js
AssistantService.js
AuthService.js
AuthService.spec.js
cleanup.js
createRunBody.js
createRunBody.spec.js
GraphApiService.js
GraphApiService.spec.js
GraphTokenService.js
initializeMCPs.js
initializeMCPs.plugins.spec.js
initializeMCPs.spec.js
initializeOAuthReconnectManager.js
initializeOAuthReconnectManager.spec.js
MCP.js 👛 fix: Drop Unread Credentials From MCP Domain Validation (#16035) 2026-09-16 22:13:49 -04:00
MCP.spec.js 👛 fix: Drop Unread Credentials From MCP Domain Validation (#16035) 2026-09-16 22:13:49 -04:00
MCPAuthorizationFenceRetry.js
MCPAuthorizationFenceRetry.spec.js
MCPRequestContext.js
OboPolicyService.js
OboTokenService.js
OboTokenService.spec.js
OpenIDRefreshFlight.js
OpenIDRefreshFlight.spec.js
OpenIDRefreshRecovery.js
OpenIDSessionRefresh.js
OpenIDSessionRefresh.spec.js
PermissionService.js
PermissionService.spec.js 🚥 fix: Preserve Insights With Guarded ACL Role Updates (#16170) 2026-09-21 23:40:52 -04:00
PluginService.js
PluginService.spec.js
RefreshTokenBridge.js
RefreshTokenBridge.spec.js
systemGrant.spec.js
ToolService.js 🌳 feat: Isolate Attached Workspaces by Conversation (#16107) 2026-09-19 23:19:26 -04:00
twoFactorService.js
twoFactorService.spec.js