Commit graph

2009 commits

Author SHA1 Message Date
Danny Avila
12fea693bb
🦥 perf: Lazy-Load Agent Version History in Editor (#13977)
Opening the agent editor fetched the full `versions` array (each a complete
config snapshot) alongside the agent, so agents with large histories were slow
to open. Version history is now loaded only when the user opens it.

- Add `getAgentWithVersionCount` (aggregation: version count, no versions array)
  and `getAgentVersions` data-schemas methods.
- `getAgentHandler` returns the version count without the heavy array; add
  `GET /agents/:id/versions` (EDIT-gated) for lazy retrieval.
- Add `useGetAgentVersionsQuery`; VersionPanel reads current config from the
  cached expanded query and fetches versions on open. Revert keeps the expanded
  cache and versions query in sync.
2026-06-26 12:19:54 -04:00
Danny Avila
b15d40e3e4
🪣 refactor: Rate-Limit Token Routes and Cap Remote File Downloads (#13978)
* harden token and remote file handling

* sort s3 storage imports

* split token submission rate limits
2026-06-26 12:19:03 -04:00
Ravi Kumar L
a0529c9af7
🪭 feat: Add opt-in Langfuse fanout gateway + collector (#13872)
* feat: add opt-in Langfuse fanout collector

* feat: fan out Langfuse feedback scores

* docs: prepare Langfuse fanout for OSS setup

* fix: clarify Langfuse fanout collector config

* test: stabilize librechat suite

* test: fix upload dialog import order

* fix: omit empty Langfuse tenant fields

* fix: gate tenant Langfuse fanout

* test: cover central Langfuse env fallback

* style: format Langfuse fanout config

* feat: route langfuse fanout by destination

* docs: clarify langfuse compose destination scope

* test: remove unrelated suite stabilization

* style: sort agent imports

* fix: treat blank tenant fanout toggle as disabled

* fix: rename tenant fanout emergency toggle

* test: guard langfuse fanout collector config drift

* feat: tune langfuse fanout batching

* test: render fanout helm tests without dependencies

* fix: narrow remote agent run config

* refactor: share string normalization helper

* fix: align langfuse fanout env parsing

* fix(langfuse): align score fanout toggles with traces

* fix(langfuse): keep central fanout config collector-only

* fix(langfuse): type fanout collector config

* fix(langfuse): harden tenant fanout config

* feat(langfuse): support media fanout gateway

* fix(langfuse): route tenant fanout through destination URL

* fix(langfuse): harden fanout routing checks

* ci(langfuse): test fanout gateway changes

* ci(langfuse): check fanout go formatting

* fix(langfuse): satisfy api typecheck
2026-06-26 11:26:39 -04:00
Danny Avila
0d1103f62f
🪙 fix: Count Quote Tokens on Message Edit (#13958)
The message-edit route recomputed a user message's tokenCount from the edited
`text` alone, ignoring its persisted `quotes`. But the send path re-prepends
those quotes into the prompt on every turn (mergeQuotedText), so after editing a
quoted message the stored tokenCount under-reported by the whole quote block,
skewing the context gauge and any other tokenCount consumer.

The full-recount path now fetches the message's quotes and counts the merged
text+quotes via a new `mergeQuotedTextForCount` helper in packages/api (mirrors
the send path), so the stored count stays authoritative. The incremental
content-part path is left as-is: it deltas only the edited part and preserves the
rest of the count (incl. the quote contribution), and applies to content-array
messages rather than text+quotes user turns.

Deferred follow-up from #13953.
2026-06-25 16:23:51 -04:00
Peter
abf9fc307d
📇 feat: Agent Contact Visibility with Owner Fallback (#13663)
* Shared Contract

* Backend Resolution

* Frontend Display

* Contact Styling and more tests

* fix contact flicker when saving an agent

* fix display owner when contact deleted

* simplification of the last fixes

* github action fixes

* fixes failing tests

---------

Co-authored-by: Peter Rothlaender <peter.rothlaender@ginkgo.com>
2026-06-25 15:58:15 -04:00
Danny Avila
376370d610
♻️ refactor: Compute Context Gauge Client-Side, Drop Projection Endpoint (#13953)
* ♻️ refactor: Compute Context Gauge Client-Side, Drop Projection Endpoint

The /api/endpoints/context-projection endpoint re-fetched a conversation's
messages from Mongo and re-tokenized them to project the context gauge for
snapshot-less branches. The browser already holds those messages and their
per-message tokenCounts, so this duplicated work on the request path (an
unbounded read + server-side BPE tokenization until it was later capped).

Move the snapshot-less estimate fully client-side, from the in-memory index:

- sumBranch accumulates an uncalibrated char/4 estimate (estTokens) for
  count-less messages (imports / pre-feature) under the same summary cutoff
- useTokenUsage folds estTokens (calibrated via the existing calibrationFamily
  ratio) into the existing fallback; known per-message counts render unchanged
- delete the endpoint, controller, rate limiter, route, the getMessageTextStats
  data-schemas method, and the data-provider surface (endpoint/key/type/service/query)

No DB read, no server tokenization, no rate-limit knobs; the gauge recomputes
reactively from the index. Net -793 lines.

* 🩹 fix: Count quotes and object-form content in client context estimate

Address Codex review on the client-side context estimate:

- messageChars now reads object-form content text (part.text.value), not
  only string text/think, so imported / pre-feature messages whose body
  lives in content parts are no longer estimated as zero.
- Count-less user messages include their merged quote excerpts in the
  estimate, mirroring what the send path prepends into the prompt.

* 🩹 fix: Cap over-window estimate and surface estimated tokens in breakdown

Address remaining Codex review on the client-side context estimate:

- Clamp the snapshot-less estimate's displayed usedTokens to maxTokens. The
  send path prunes an over-window branch before calling the model, so the
  gauge never actually exceeds the window; this avoids impossible values
  (e.g. 50k / 8k) without re-introducing client-side pruning.
- Surface the calibrated count-less estimate as its own "Estimated" row in
  the breakdown popover, so a branch of only count-less imported / pre-feature
  messages is no longer shown as Input 0 / Output 0 under a non-zero header.

* 🩹 fix: Refine client context estimate per Codex re-review

- Drop calibration from the snapshot-less estimate. The removed projection
  never actually calibrated (the client never sent a ratio), and a ratio
  inflated by provider-injected context over-estimates visible imported text.
- Exclude reasoning (think) / error parts from the estimate; the send path
  strips them, so they are not part of the next call's context.
- Fold quote text into the estimate even when a tokenCount is present, since
  the edit route recounts tokenCount from text only and drops the merged quote.

* 🩹 fix: Recount quoted user turns instead of topping up the stored count

The previous round added quote chars on top of a quoted message's stored
tokenCount, which double-counts the common (unedited) case where the count
already includes the merged quote prompt. Match the removed projection
instead: for quoted user turns, ignore the stored count and estimate the
full merged text. This both avoids the double-count and still corrects the
stale text-only count an edit leaves behind.

* 🩹 fix: Trust stored counts for quoted turns; count tool-call parts

- Quoted user turns: revert to trusting a present tokenCount. The send path's
  stored count already includes the merged quote (and any calibration), and
  the client's char/4 path is coarser, so recounting regressed normal turns.
  Only count-less messages estimate quotes from text.
- Count tool-call name/args/output for count-less assistant messages; the
  formatter sends them back as context, so omitting them under-reported
  imported branches with tool history.

* 🩹 fix: Exclude in-flight tail from estimate to avoid resume double-count

On resume the live path seeds liveTokens from the partial response and also
writes that content into the messages cache, where the count-less response
is estimated into estTokens too — double-counting the in-flight output on the
snapshot-less estimate path. sumBranch now exposes the tail message's own
estimate (tailEstTokens); the estimate path drops it while a stream is live,
so the in-flight response is counted once (via liveTokens). The breakdown's
Estimated row uses the same in-flight-adjusted value.

* 🩹 fix: Recount quoted user turns in context estimate (match send path)

A quoted user turn's stored tokenCount is unreliable for the gauge: a
text-only Save edit recomputes it from text alone, and the send path
(needsCanonicalTokenCount in agents/client.js) recounts the quote-merged
prompt every turn regardless of the stored value. Mirror that on the client
— estimate quoted turns from the merged text+quotes and ignore the stored
count — so snapshot-less branches don't under-report by the quote block.
Reverts the earlier "trust the count" assumption, which the server disproves.

* 🧹 chore: Route useResumableSSE diagnostics through the frontend logger

Convert the [ResumableSSE]/[Debug] console.log and console.error diagnostics
to the gated frontend `logger` (client/src/utils/logger), splitting the tag
from the message so object arguments are passed through as real args (logged
expandably, not stringified) and the logs stay tag-filterable and off the
production console unless explicitly enabled. All log statements preserved;
nothing removed.

* 🩹 fix: Prefer content over text when estimating count-less messages

A stopped agent response is saved with both a `text` field and a structured
`content` array, and the send path formats from content. messageChars
early-returned on `text`, dropping the content array (and the tool-call tokens
it carries) from the snapshot-less estimate — also making the tool_call
handling dead for such messages. Prefer content when present, fall back to text.
2026-06-25 15:29:31 -04:00
Danny Avila
1dffa100bb
📦 chore: bump @librechat/agents to v3.2.52 (#13939)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-06-24 17:54:57 -04:00
Danny Avila
397ddc5366
🧠 feat: Add Memory as an Agent Capability with Inline Tools and Ephemeral Badge (#13869)
* 🧠 feat: Memory Agent Capability with Inline Tools and Ephemeral Badge

Add `AgentCapabilities.memory`, which expands into the inline set_memory/delete_memory tool pair (mirroring the execute_code expansion via registerMemoryTools) when a run-level memoryAvailable gate holds: capability enabled, memory configured, MEMORIES.USE permission, and personalization not opted out. Surfaces the memory artifact as an attachment in the agents tool-end callback.

Adds the ephemeral path (TEphemeralAgent.memory, load/added agent tool injection), a fully-gated memory badge plus tools-dropdown entry, the agent-builder Memory toggle with form round-trip, and a mock e2e test asserting the badge reaches the request payload. Additive to and independent of the existing post-turn memory extraction agent.

* 🩹 fix: Address Codex review on memory capability (gating, validKeys, usage guard)

- Strip the memory capability from the served agents capabilities when memory is not configured/enabled, so the badge, tools dropdown, agent-builder toggle, and backend capability gate stay consistent instead of exposing an inert toggle on default installs (where MEMORIES.USE defaults true).
- Surface configured memory.validKeys in the inline tool definitions so the model is told the allowed keys up front, matching the runtime createMemoryTool schema.
- Append a strict explicit-request usage guard to the agent instructions when inline memory tools are registered, preserving the memory-agent's privacy behavior.
- Add AppService tests covering memory-capability stripping.

*  test: Update AppService capability snapshots for memory strip

AppService now strips the memory capability from the served agents defaults when no memory block is configured; update the spec's expected capability lists to defaultAgentCapabilitiesWithoutMemory for the no-memory-config cases.

* 🛡️ fix: Address Codex re-review on memory capability (round 2)

- Strip the memory capability from the FINAL served agents config, not just defaults; loadEndpoints reparses any endpoints.agents block, so memory was still exposed in that common shape (packages/data-schemas/src/app/service.ts) + regression test.
- Re-check the full memory gate (config, opt-out, MEMORIES.USE) inside handleTools before constructing set_memory/delete_memory, so an unsolicited tool call from a model/custom endpoint can't bypass the runtime gates (api/app/clients/tools/util/handleTools.js).
- Restore the persisted memory toggle for model-spec conversations via applyModelSpecEphemeralAgent (client/src/utils/endpoints.ts).
- Clear LAST_MEMORY_TOGGLE_ on logout and clear-all-chats so a stale memory preference can't leak across users on a shared browser (client/src/utils/localStorage.ts).

* 🧠 fix: Address Codex re-review on memory capability (round 3)

- Serialize set_memory writes and advance a running token total inside createMemoryTool, so parallel batched calls in one event-driven turn can't each pass the limit check against a stale total and collectively exceed memory.tokenLimit (packages/api/src/agents/memory.ts) + tests.
- Inject the keyed memory context (withKeys) instead of withoutKeys when the running agent has the inline memory capability, so delete_memory has a visible key to target (api/server/controllers/agents/client.js).

* 🔐 fix: Address Codex re-review on memory capability (round 4)

- Detect inline memory by tool NAME (set_memory/delete_memory) across an initialized agent's tools + toolDefinitions, since the 'memory' marker is expanded at init and the prior string check never matched; inject the keyed memory context for any primary OR sub-agent that carries the inline memory tools (api/server/controllers/agents/client.js).
- Enforce memory WRITE permissions in the inline tool gate: set_memory requires CREATE+UPDATE and delete_memory requires UPDATE (matching the REST memory routes), so a USE-only role can't mutate/delete memories via agent tool calls (api/app/clients/tools/util/handleTools.js).

* 🔒 fix: Address Codex re-review on memory capability (round 5)

- Gate inline memory registration (memoryAvailable) on the memory WRITE permissions (USE+CREATE+UPDATE), so a read-only-memory role no longer has set_memory/delete_memory shown to the model only for the runtime loader to refuse them (api/server/services/Endpoints/agents/initialize.js).
- Enforce the per-agent memory opt-in at execution: handleTools now refuses to construct set_memory/delete_memory unless the agent actually declared them (toolDefinitions/tools), blocking hallucinated/undeclared memory tool calls from mutating memory.
- Fail closed when getFormattedMemories errors with a configured tokenLimit, instead of writing as if storage were empty and bypassing the cap (api/app/clients/tools/util/handleTools.js).

* 🩹 fix: Address Codex re-review on memory capability (round 6)

- Fix a P1 regression from the prior round: the execution-context agent keeps the raw 'memory' capability marker (not the expanded set_memory/delete_memory names), so the opt-in check now matches the marker. This restores memory writes/deletes AND avoids hijacking an MCP tool that merely shares the set_memory/delete_memory name (api/app/clients/tools/util/handleTools.js).
- Count repeated set_memory writes to the same key as replacements, not additions, against tokenLimit — set_memory upserts, so a same-key rewrite swaps its prior token contribution instead of double-counting (packages/api/src/agents/memory.ts) + test.
- Gate the memory badge, tools dropdown, and agent-builder toggle on the full memory write permissions (USE+CREATE+UPDATE) via a shared useHasMemoryAccess hook, so a read-only-memory role no longer sees an enabled Memory control the backend would refuse to wire up.

* 🧷 fix: Address Codex re-review on memory capability (round 7)

- Recognize inline memory across both execution-context agent shapes: initializeAgent now sets a LibreChat-only memoryToolsRegistered flag on the InitializedAgent, and the opt-in/detection checks accept that flag OR the raw 'memory' marker. Fixes memory failing for processAddedConvo agents (which store the initialized config, marker already expanded) while staying MCP-name-collision-safe (api/app/clients/tools/util/handleTools.js, packages/api/src/agents/initialize.ts, api/server/controllers/agents/client.js).
- Scope keyed memory context to memory-enabled agents only: useMemory now returns both keyed and unkeyed contexts, and buildMessages injects the keyed one (memory keys + token metadata) only to agents that can call delete_memory, while the primary/post-turn path keeps the unkeyed values — so a primary without memory tools no longer sees memory keys it doesn't need.

* 🔏 fix: Address Codex re-review on memory capability (round 8)

- Enforce memory size limits on inline writes: createMemoryTool now rejects keys over 1000 chars and values over memory.charLimit, matching the REST memory routes, so an inline-memory agent can't persist blobs the memory UI/API would reject (packages/api/src/agents/memory.ts, api/app/clients/tools/util/handleTools.js) + test.
- Recheck the agents 'memory' endpoint capability at execution time, so a stale/hallucinated set_memory/delete_memory call can't mutate memory after an admin removes the capability while the agent document still carries the marker (api/app/clients/tools/util/handleTools.js).

* ♻️ refactor: Move inline-memory backend logic into packages/api + share memory load

Workspace boundary: the inline-memory gating/detection logic that had crept into /api now lives in packages/api/src/agents/memory.ts (TS), with /api kept as thin wrappers.

- Add agentHasInlineMemoryTools, isMemoryToolAllowed, and buildInlineMemoryTool to packages/api; handleTools.js now calls buildInlineMemoryTool instead of constructing/gating the tools inline, and client.js imports agentHasInlineMemoryTools instead of redefining it.
- Optimize repeated memory loads: getRequestMemories memoizes getFormattedMemories per request (WeakMap keyed by req), so the run's memory-context load and every memory-enabled agent's set_memory token-usage load share a single DB fetch instead of one per agent.

* 🧠 fix: Invalidate request memory cache after inline writes

Inline set_memory/delete_memory now invalidate the request-scoped
getFormattedMemories cache on a successful write, so a later tool round
in the same response is seeded with the post-write usage total instead
of the stale pre-write one (multi-round writes no longer collectively
exceed tokenLimit, and a set after a delete is not over-counted). The
within-round sharing across multiple memory-enabled agents is preserved.

* 🧠 fix: Persist memory capability on saved agents; honor registration flag

- Add Tools.memory to the v1 systemTools allowlist so filterAuthorizedTools
  no longer silently drops the memory marker when an agent with the Memory
  capability is created/updated/duplicated through the builder (previously
  the capability only worked for ephemeral chats, not persisted agents).
- agentHasInlineMemoryTools now honors an explicit memoryToolsRegistered
  boolean before falling back to the raw `memory` marker, so an initialized
  config whose registration was denied (memoryAvailable false) is not given
  keyed memory context just because the marker survives in tools.

* 🧩 fix: Bring memory tool to parity with other ephemeral tools

- Add `memory` to the model-spec schema/type and honor `modelSpec.memory`
  in both ephemeral paths (load.ts, added.ts) and the frontend spec
  application, so admins can pre-enable Memory from a model spec exactly
  like webSearch/fileSearch/executeCode.
- Add LAST_MEMORY_TOGGLE_ to the timestamped-storage cleanup list so stale
  per-conversation memory toggles are purged on startup like the others.
- Hide the agent-builder Memory toggle for users who disabled memory in
  personalization (memories === false), mirroring the chat badge's opt-out
  gate, so the setting isn't shown as inert/misleading.

*  test: Cover memory in applyModelSpecEphemeralAgent spec defaults

Update the exact-object assertions to include the new `memory` field and
add positive coverage that `modelSpec.memory` maps to the ephemeral
agent's `memory` flag. Fixes the shard 2/4 failure from 672a03b05.
2026-06-24 17:14:13 -04:00
Danny Avila
771b93bf10
🪝 feat: HITL Tool Approval Scaffolding (Slice A) (#12938)
* 🪝 feat: HITL Tool Approval Scaffolding

Adds the foundational types, job-state, config schema, and policy module
for human-in-the-loop tool approval. Purely additive — no behavior change
on existing runs. Lands ahead of the agents-SDK interrupt/checkpointer
integration so both tracks can land independently.

- LangChain HumanInterrupt-shaped types in `Agents.*` namespace
  (`HumanInterruptPayload`, `ToolApprovalRequest`, `ToolReviewConfig`,
  `PendingAction`, `ToolApprovalResolution`); `ToolCall`/`ToolCallDelta`
  gain an optional `approval` field.
- New `requires_action` job status (non-terminal) plus `pendingAction`
  field on `SerializableJobData` and `GenerationJobMetadata`. Both stores
  treat the status as paused-but-alive; Redis `updateJob` has explicit
  `requires_action`/`running` transition branches that refresh the hash
  TTL, manage the `runningJobs` set, and `HDEL pendingAction` on resume.
  Both stores include `requires_action` in `getActiveJobIdsByUser`.
- `GenerationJobManager` gains `markRequiresAction`, `getPendingAction`,
  `clearPendingAction`; `getJobCountByStatus` aggregates the new status.
- `endpoints.agents.toolApproval` config (`default`/`required`/`excluded`)
  and a policy module exporting `decideToolApproval`, `requiresApproval`,
  and `buildPendingAction` (the LangChain-shaped payload builder).
- 20 unit tests covering policy resolution and the manager lifecycle.

* 🧭 refactor: Align HITL Surface with Agents SDK Permissions Model

Reshapes Slice A on top of the agents SDK's now-landed HITL surface
(`createToolPolicyHook`, discriminated `HumanInterruptPayload`, `'bypass'`
mode naming). Host stops reimplementing evaluation logic and becomes a
config mapper + payload wrapper.

Schema (data-provider):
- `toolApproval` shape now mirrors SDK `ToolPolicyConfig` 1:1:
  `mode: 'default' | 'dontAsk' | 'bypass'`, plus `allow` / `deny` / `ask`
  glob lists and an optional `reason` template. `enabled` is the
  LibreChat-only admin kill switch.
- `'bypass'` (not `'bypassPermissions'`) — matches the SDK's surface.

Types (`Agents.*` namespace):
- `HumanInterruptType` extended to `'tool_approval' | 'ask_user_question'`.
- `HumanInterruptPayload` is now a discriminated union — `tool_approval`
  carries `action_requests` + `review_configs`; `ask_user_question`
  carries a free-form question with optional curated options.
- New: `AskUserQuestionRequest`, `AskUserQuestionOption`,
  `AskUserQuestionResolution`.
- `ToolApprovalDecision` (string union) renamed to
  `ToolApprovalDecisionType` to free the `Decision` name for the SDK's
  discriminated object union later.
- `ToolApprovalResolution` gains `reason?` and `scope?: 'once' | 'session'
  | 'always'` so route signatures stabilize before persistence lands.

Policy module (`packages/api/src/agents/hitl/policy.ts`):
- Drop `decideToolApproval` / `requiresApproval` / `ToolRef` — the SDK's
  `createToolPolicyHook` handles full evaluation
  (`deny → bypass → allow → ask → dontAsk → fallthrough(ask)`).
- Add `isHITLEnabled(policy)` — the kill-switch predicate that gates the
  SDK's `humanInTheLoop: { enabled: false }` opt-out in Slice B.
- Add `mapToolApprovalPolicy(policy)` — strips `enabled`, returns a
  `ToolPolicyConfig` to feed `createToolPolicyHook`. Structural mirror of
  the SDK type so this compiles before the SDK upgrade ships.
- Reshape `buildPendingAction(payload, ctx)` to wrap any
  `HumanInterruptPayload` with job context — accepts SDK output directly.
- Add `buildToolApprovalPayload(...)` and `buildAskUserQuestionPayload(...)`
  helpers for synthesizing payloads in tests / pre-SDK flows.

Tests:
- 22 new unit tests covering the mapper, predicate, and payload builders;
  20 → 27 total pass across policy + manager-lifecycle suites.

* 🪢 chore: Import ToolPolicyConfig From `@librechat/agents`

The SDK type now ships in 3.1.77 (already pinned on `dev`), so the
structural mirror in `policy.ts` is redundant. Drop the local interface
and import directly so future SDK changes to `ToolPolicyConfig` propagate
without our `mapToolApprovalPolicy` going stale.

* 🔑 fix: Carry tool_call_id On ToolReviewConfig (HITL)

`ToolReviewConfig` was joining with `ToolApprovalRequest` by position only.
That breaks the moment a single batch contains the same tool called twice
(e.g. a model fanning out parallel `mcp:server:search` calls): the UI can't
tell which review config applies to which action request once it filters
or reorders.

Mirrors the SDK's `ToolApprovalReviewConfig` shape — `tool_call_id` is the
join key, `action_name` is retained for display only.

Also: drop a JSDoc warning on `isHITLEnabled` so a future contributor doesn't
wire `humanInTheLoop: { enabled: true }` without supplying a host
checkpointer — the SDK's `MemorySaver` fallback is process-local and
silently breaks resume across worker hops.

- `Agents.ToolReviewConfig` adds `tool_call_id: string`
- `buildToolApprovalPayload` populates `tool_call_id` per review config
- New test covers the duplicate-tool batch case (two parallel calls to
  the same tool); 27 → 28 tests

* fix: Address HITL review findings

* fix: Refresh paused HITL Redis state

* test: Stabilize HITL abort fallback specs

* 🎨 style: Sort imports to satisfy dev lint gate (HITL)

* 🏛️ refactor: Deepen HITL approval lifecycle into one race-safe seam

Architecture-review candidate #1 (+ #4). The requires_action lifecycle was
three shallow pass-throughs over updateJob with the legal transitions
smeared across JSDoc, the JobStatus union, and each store adapter — and the
resume transition was NOT race-safe: the Redis lua checked existence, not
status, so two concurrent approval submits both drove the run (re-executing
tools / double-billing).

- IJobStore.transitionStatus: atomic compare-and-set status transition that
  only fires if the job is currently `from`. InMemory: sync compare. Redis:
  single-node lua with a status guard (cluster best-effort, matching the
  existing posture); reconciles membership sets + TTLs to `to`.
- New ApprovalLifecycle module: pause / peek / resolve / expire — guarded,
  race-safe transitions behind one interface. resolve() returns true to
  exactly one concurrent caller; the previously-undefined
  requires_action → aborted expiry edge is now explicit; peek treats
  past-expiresAt as gone (lazy expiry).
- GenerationJobManager exposes `approvals` and delegates; the three shallow
  methods (mark/get/clearPendingAction) are removed — callers cross the deep
  interface.
- #4: typeContract.spec asserts the SDK <-> data-provider HITL types stay
  compatible (fails the build on drift); RedisJobStore validates the
  pendingAction shape on deserialize instead of a bare JSON.parse (defends
  the cold-resume path against malformed/stale records).
- Tests rewritten at the deep interface: double-resolve wins once,
  pause-on-terminal rejected, explicit expiry, lazy-expiry peek.

No Slice B wiring — this deepens the existing scaffolding so the future
resume route and run seam are born crossing one race-safe interface.

* 🛡️ fix: Address Codex review on the HITL approval lifecycle

Seven findings on the lifecycle deepening (089ba09f9), all valid:

- F3 actionId guard: resolve/expire take an expectedActionId; pause records a
  flat `pendingActionId` the atomic CAS guards on, so a stale decision can't
  resume a job that has since paused for a different action.
- F4 cluster single-winner: transitionStatus now decides the winner with an
  atomic CAS on the single-slot job hash (one Lua, cluster-safe), then
  reconciles cross-slot membership sets — two concurrent resolves can no
  longer both win on Redis Cluster.
- F1 resume reaping: resolve refreshes `lastActiveAt`; both stores' stale-
  running failsafes key off it, so a long-paused approval isn't reaped right
  after resuming.
- F2 expire completedAt: expire writes completedAt so terminal cleanup
  reclaims the job (InMemory only cleans terminal jobs with completedAt set).
- F5 facade: buildJobFacade copies pendingAction into metadata so status/
  resume routes can render the prompt.
- F6 resume metadata: PendingAction + buildPendingAction carry the SDK
  interruptId/threadId needed to rebuild Command({ resume }) cross-process.
- F7 mirror: data-provider AskUserQuestionRequest gains optional description.

Tests added at the interface: stale-actionId resolve rejected, expire sets
completedAt. tsc + lint clean; policy + type-contract specs pass.

* 🛡️ fix: Address Codex round 2 on the HITL Redis adapter

Five P2 findings on abf4b86291, all valid Redis-adapter consequences of
round 1:

- G1 terminal cleanup on expiry: transitionStatus's terminal path now runs
  the same chunk/run-step/userJobs cleanup as updateJob (extracted into a
  shared applyTerminalContentCleanup). Expired approvals no longer leave
  Redis stream contents around for the full running TTL.
- G2 pause via updateJob mirrors pendingActionId, so a pause through the
  generic path carries the flat field the stale-decision guard compares.
- G3 resume via updateJob refreshes lastActiveAt (and clears pendingActionId),
  matching transitionStatus so a long-paused job isn't reaped post-resume.
- G4 getActiveJobIdsByUser excludes a requires_action job whose pendingAction
  is past expiry (both stores), via shared isPendingActionExpired — the client
  stops polling an expired prompt.
- G5 createJob clears stale pendingAction/pendingActionId/lastActiveAt on a
  reused streamId, so a fresh run never exposes a prior run's approval metadata
  and cleanup keys off the new createdAt.

Tests added: expired pending-approval excluded from the active set. tsc +
lint clean; policy + type-contract specs pass.

* 🛡️ fix: Address Codex round 3 — approval expiry lifecycle completeness

Three P2 findings on 780833d908, all valid:

- H1 status consistency: /chat/status now treats a non-expired
  requires_action job as active (matching /chat/active), so a client
  refreshing while an approval is pending resumes/subscribes instead of
  treating the run as finished and stranding it.
- H2 active expiry: cleanup now finalizes past-expiry requires_action jobs
  (→ aborted) in both stores instead of only filtering them from the active
  list — an expired prompt no longer lingers resident until key TTL. Redis
  routes through transitionStatus (terminal content cleanup); in-memory marks
  terminal + reclaims.
- H3 resumed liveness: in-memory stale-running check uses
  max(lastActivity, lastActiveAt, createdAt), so a just-resumed job isn't
  reaped on a stale per-chunk lastActivity entry before the next chunk.

Test added: in-memory cleanup finalizes + reclaims a past-expiry approval.
tsc + lint clean; policy + type-contract specs pass.

* 🛡️ fix: Address Codex round 4 — paused-job edge cases across the stack

Five P2 findings on 4324a4e776, all valid:

- I1 message validation: validateMessageReq's active-job read bypass now
  accepts a live requires_action job, so a new-conversation run that pauses
  before its final save can recover the prompt instead of 404ing.
- I2 expire targets the observed record: resolve()'s expired path passes
  `expectedActionId ?? job.pendingAction.actionId`, so a concurrent
  resume+re-pause can't let expire abort a different action.
- I3 stale/malformed prompts: new isPendingActionStale (missing OR expired)
  drives active-listing exclusion + cleanup expiry in both stores, and the
  status route + middleware require a live pendingAction — a requires_action
  job whose pendingAction was dropped on deserialize no longer reads active.
- I4 in-memory parity: InMemory updateJob mirrors pendingActionId on pause and
  clears it + refreshes lastActiveAt on resume (matching RedisJobStore), so a
  pause via the generic path is still resolvable by actionId.
- I5 long approval windows: paused-job live TTL (job/chunks/run-steps) now
  covers pendingAction.expiresAt + grace (pauseTtlSeconds), on both the
  transitionStatus and updateJob pause paths, so Redis can't evict a paused
  job before its decision window closes.

tsc + lint clean; policy + type-contract specs pass.

* 🛡️ fix: Codex round 5 — refuse unresolvable resolves; expose pending action

Two of three findings on c8abd826e1 (the third deferred to Slice B):

- J3 resolve() refuses a requires_action job that has lost its pendingAction
  (e.g. a malformed record dropped on deserialize): it expires/finalizes the
  job instead of driving a resumed run with no reviewed interrupt payload —
  consistent with how active-listing + cleanup already treat a stale prompt.
- J2 /chat/status returns the live pendingAction for a paused stream, so a
  client rebuilding from status (reload / cross-replica) has the action id +
  payload to render and submit the prompt, not just "paused".

Deferred (Slice B): J1 — emitting a terminal SSE event on approval expiry so
already-subscribed clients close. The store-level lifecycle can't emit
transport events, and there are no live SSE subscribers to a paused stream
until the Slice B runtime wiring exists; tracked for that work.

tsc + lint clean; policy + type-contract specs pass.

* 🛡️ fix: Codex final round — paused-job TTL + pendingAction in resume contract

Two of three findings on e7d9cf21b6 (third deferred to Slice B):

- K2 paused-job TTL: a paused (requires_action) job no longer inherits the
  20-minute running TTL — it uses a dedicated requires_action backstop
  (default 24h, configurable) so a no-expiry approval (the buildPendingAction
  default), which the API treats as live, isn't evicted by Redis mid-window.
  A longer pendingAction.expiresAt still extends beyond the backstop.
- K3 resume contract: pendingAction is now carried on the typed ResumeState
  (data-provider) and populated by getResumeState for a live paused job, so a
  reloading / cross-replica client can rebuild the prompt from resumeState
  (the contract useResumeOnLoad actually reads), not just a loose status field.

Deferred (Slice B): K1 — emit a terminal SSE event on expiry so already-
subscribed clients close. Requires the manager/eventTransport layer (the
store-level lifecycle and cleanup loops have no transport access) and has no
live subscriber until the Slice B subscribe/resume path exists; tracked there.

tsc + lint clean; policy + type-contract specs pass.

* ♻️ refactor: dedup HITL transition path + liveness predicate (arch review)

Two follow-ups from the post-hardening architecture re-review — both pure
dedup, no behavior change:

A — collapse the dual status-transition path. transitionStatus is now the
   sole membership-aware transition (running ⇄ requires_action). Removed the
   updateJob requires_action/running branches and the now-orphaned
   transitionToRequiresAction / transitionToRunning / refreshLiveJobTtls, plus
   the per-store pause/resume mirror logic that had to be re-synced into parity
   across review rounds (G2/G3/I4/I5). updateJob is back to a plain field
   writer + terminal cleanup. The Redis integration tests that drove
   updateJob({status}) now drive transitionStatus (the real path).

B — one canonical "is this approval live?" predicate. isPendingActionStale /
   isPendingActionExpired are exported from @librechat/api and used by the
   stores, ApprovalLifecycle (dropped its private isExpired), the /chat/status
   route, and validateMessageReq — replacing 3 inlined re-derivations that were
   the drift source behind several review findings.

tsc + lint clean; policy + type-contract specs pass. Redis integration specs
(migrated) are CI-verified.
2026-06-24 16:47:16 -04:00
Marco Beretta
61016e328a
🔄 feat: Continue Shared Conversations as Personal Copies (#13714)
Adds a "Continue this chat" button to the shared conversation view that forks
the shared conversation into a new conversation owned by the viewer and opens it
to continue (issue #13001).

- POST /api/share/:shareId/fork, gated by requireJwtAuth, the fork rate
  limiters, and the canAccessSharedLink ACL (view access = fork access).
- forkSharedConversation clones from the anonymized getSharedMessages payload,
  so only share-visible data is copied.
- Strips file ids from cloned files/attachments so a fork grants no more file
  access than viewing the read-only share, and honors the global shared-file
  kill switch via the snapshotFiles option.
- Reduces the clone to the viewer's active branch, located by its index in the
  shared payload (shared ids are re-anonymized per request and createdAt can
  collide, while the payload order is stable).
- Resolves config/retention, persists, and reads back under the requesting
  user's tenant, not the share owner's; canAccessSharedLink also falls back to
  a system-wide share lookup so cross-tenant public shares resolve (ACL still
  enforced under the share's own tenant).
- Resolves a usable endpoint/model from the viewer's models config instead of
  hard-coding OpenAI, so deployments without OpenAI can send the first message.
- Routes the fork's 401s (logged-out or cold-loaded viewers) through login,
  including when the refresh itself is rejected for a stale session.
- Hides the Temporary Chat toggle once a conversation has a real id, and
  portals the share-settings theme/language dropdowns above the dialog.

Rebased onto dev; collapses the share-fork feature and its review fixes into a
single commit.
2026-06-24 16:27:01 -04:00
Marco Beretta
b84e26671e
🕒 feat: Track Terms Acceptance Timestamp (#10810)
* feat: add terms acceptance timestamp tracking and migration script

* feat: update migration script to use countUsers method for user count

* Update config/migrate-terms-timestamp.js

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

* feat: enhance terms acceptance response to include acceptance timestamp

* fix: make terms acceptance idempotent and fail migration on partial errors

Preserve the original termsAcceptedAt on repeat accepts within a terms
cycle so retried or duplicate requests no longer overwrite the first
acceptance time. Exit the migration script with a non-zero status when
any per-user update fails so partial failures are not reported as
successful.

* style: fix import ordering in data-provider mutations

* refactor: record terms acceptance atomically to preserve first-accept time

Replace the read-then-write in acceptTermsController with a single
atomic acceptTerms method that conditionally stamps termsAcceptedAt via
an $ifNull aggregation update. This removes the TOCTOU window where two
concurrent first-time accepts could overwrite the earlier acceptance
timestamp, while still preserving an existing timestamp and backfilling
legacy accepted users.

* fix: run terms timestamp migration under system tenant context

Wrap the count, cursor scan, and per-user updates in runAsSystem so the
tenant isolation plugin does not throw under TENANT_ISOLATION_STRICT or
scope the cross-tenant migration to a non-existent tenant, matching the
other maintenance migrations.

* fix: guard terms backfill against concurrent acceptances

Add the missing-timestamp predicate to the per-user updateOne filter so
a user who accepts through the API between the cursor read and the write
keeps their real acceptance time instead of being overwritten with
createdAt. Track modified vs skipped so the summary reflects skips.

* fix: scope terms backfill to still-accepted users

Add termsAccepted: true to the per-user updateOne filter so a reset that
clears acceptance between the cursor read and the write is not re-stamped
with createdAt, which would otherwise poison the next acceptance cycle
through the $ifNull preserve in acceptTerms.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-24 16:26:42 -04:00
Danny Avila
9e74cc0e57
v0.8.7 (#13907)
Some checks failed
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
2026-06-24 14:49:32 -04:00
Danny Avila
1662adc581
📺 feat: Google URL Context Param with Native YouTube Video Understanding (#13924)
*  feat: Add Google url_context Param with Native YouTube Video Understanding

Mirror the web_search grounding wiring for a new Google/Gemini `url_context`
model param (resolves to the native `urlContext` tool). When enabled, YouTube
URLs in the latest user message are injected as Gemini video parts (fileData),
since the URL Context tool does not support YouTube.

* 🎞️ fix: Provider-aware YouTube injection limits for url_context

Address Codex review on the YouTube video-understanding path:
- Cap injected YouTube parts per request by provider/model (Vertex: 1; Gemini
  Developer API: 10 on 2.5+, 1 on earlier models) so multi-link messages cannot
  exceed the provider limit and get rejected.
- Set a video/mp4 mimeType on Vertex YouTube fileData (matching Vertex samples);
  the Developer API still omits it.

* 🧩 fix: Round-trip url_context for Google-compatible custom endpoints

Add url_context to openAIBaseSchema so the per-chat value persists for custom
endpoints configured with customParams.defaultParamsEndpoint: 'google', matching
how web_search is already picked there.

* 🚦 fix: Gate url_context tool to Gemini 2.5+ models

Per Google's URL Context supported-models list (2.5+/3.x only), skip the native
urlContext tool on earlier models (debug-log + no-op) instead of sending it and
triggering a provider 400. This also gates the coupled YouTube video-understanding
injection to 2.5+, since it keys off the resolved urlContext tool.

* ✂️ fix: Strip YouTube URLs from urlContext text; keep url_context out of OpenAI schema

- Remove url_context from the shared openAIBaseSchema (revert): it is Google-only
  and would otherwise leak as an unsupported param to OpenAI/Azure/OpenRouter
  requests. On Google-compatible custom endpoints url_context is enabled via admin
  addParams/defaultParams, same as web_search.
- When injecting YouTube video parts, strip the matched YouTube URLs from the prompt
  text so the urlContext tool (which reads URLs from text and cannot fetch YouTube)
  does not consume its URL budget on them. Non-YouTube URLs are left intact.

* 🎯 fix: Refine url_context model gating and YouTube injection edges

Address Codex round 4:
- Exclude non-text modality variants (image/live/tts) from URL Context support,
  mirroring the Google tool-combination modality exclusion.
- Use the resolved run model (model_parameters.model) for YouTube injection limits
  instead of the saved base model.
- Strip only the YouTube links actually routed to video (id-aware); keep over-limit
  links in the text so the model can still reason about them.
- Keep timestamped YouTube links (?t=/&start=) in the text so the moment cue survives.
- Recognize youtube-nocookie.com/embed links.

* 🎚️ fix: Exclude audio Gemini variants + preserve pre-id YouTube timestamps

Address Codex round 5:
- Add `audio` to the url_context modality exclusion so audio-only Gemini variants
  (e.g. gemini-2.5-flash-preview-native-audio-dialog) skip the tool instead of 400ing.
- Detect YouTube timestamps anywhere in the matched URL (incl. before `v=`, e.g.
  watch?t=90&v=<id>), so timestamped links are kept in the prompt text as intended.
2026-06-23 22:42:06 -04:00
Danny Avila
d9a76fca90
🧠 feat: Configurable Reasoning Replay for Custom Endpoints (#13921)
* 🧠 feat: Configurable Reasoning Replay for Custom Endpoints

Adds customParams.includeReasoningContent so OpenAI-compatible custom endpoints (e.g. Xiaomi MiMo, Kimi) can replay reasoning_content on tool-call turns natively, without impersonating the moonshot provider.

* 🔁 feat: Replay reasoning_content across turns for opted-in custom endpoints

Extends the DeepSeek reasoning-content format spoof to honor customParams.includeReasoningContent, so custom OpenAI-compatible endpoints (Xiaomi MiMo, Kimi) reconstruct reasoning_content from persisted history on later turns, matching DeepSeek thinking-mode parity. Adds shouldReplayReasoningContent predicate (tested) and surfaces the flag on the initialized agent.

* 🪢 refactor: Split within-run vs cross-turn reasoning replay flags

moonshot only replays reasoning_content within a run's tool calls, not across turns. Decouples the two: includeReasoningContent = within-run replay (exact moonshot parity), new includeReasoningHistory = cross-turn reconstruction from persisted history (implies includeReasoningContent, since reconstruction is a no-op without the within-run replay flag).

* 🩹 fix: Apply reasoning replay across all param-format branches

Move the within-run includeReasoningContent application out of the OpenAI-only branch in getOpenAIConfig to after the branch dispatch, so custom endpoints using anthropic/google defaultParamsEndpoint gateway modes also honor includeReasoningContent/includeReasoningHistory. Addresses Codex finding.

* chore: Update @librechat/agents to v3.2.46

* 🧽 refactor: De-spoof reasoning replay via explicit preserveReasoningContent

Now that @librechat/agents 3.2.46 exposes an explicit preserveReasoningContent option on formatAgentMessages, pass it directly instead of impersonating provider: deepseek. Behavior is unchanged (shouldReplayReasoningContent still gates DeepSeek + the custom includeReasoningHistory flag); also corrects the comment to reference includeReasoningHistory.

* 🌳 fix: Walk subagents in the reasoning-history replay gate

The gate only checked the primary agent and top-level handoff/parallel configs, so an opted-in custom endpoint used solely as a nested subagent had its persisted reasoning dropped on later turns. New exported anyAgentReplaysReasoningContent walks subagentAgentConfigs (cycle-safe, mirrors anyAgentHasCodeEnv); client.js uses it. Addresses Codex finding.
2026-06-23 21:08:47 -04:00
Danny Avila
1eb460eb03
🧾 fix: Harden Historical File Authorization (#13918)
* fix: Harden historical file authorization

* chore: Sort file authorization imports

* fix: Preserve authorized historical artifact refs

* chore: Format historical artifact hardening
2026-06-23 15:49:57 -04:00
Danny Avila
bc6b032421
🛑 refactor: Demote User Abort Logs (#13904)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (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 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
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: Demote user abort logging

* fix: Handle abort causes

* fix: Demote user-aborted agent completion to debug log

The error users still saw originated in AgentClient's completion catch,
which logged every caught error (including user aborts) at error level
before checking the abort signal. Branch on abortController.signal.aborted
so user-initiated aborts log at debug while real failures stay error-classified.

Also give the handleAbortError it.each cases distinct titles.
2026-06-23 09:55:21 -04:00
Danny Avila
77854decdf
🪣 fix: Cap Context Projection Workload Before Tokenization (#13910)
* fix: bound context projection workload

* fix: Address context projection CI failures

* fix: Bound context projection database reads

* fix: Sort projection spec imports

* fix: Cap projection body reads with stats
2026-06-23 08:43:09 -04:00
Danny Avila
ddc763595a
🍪 fix: Validate Shared-File Cookie Auth Against the Live Refresh Session (#13908)
* fix: validate shared file cookie sessions

* fix: run shared file session lookup as system
2026-06-23 08:32:28 -04:00
Danny Avila
edc0aebdb9
🛂 fix: Re-Check execute_code Authorization on Event-Driven Tool Loads (#13912) 2026-06-23 08:30:39 -04:00
Danny Avila
e807c63d5d
🔐 fix: Gate Shared Startup Config By Link Access (#13897)
* fix: gate shared startup config by link access

* fix: satisfy shared config CI checks

* fix: align shared config client types

* fix: reject expired shared link access
2026-06-23 08:28:37 -04:00
Danny Avila
1505fd5262
📦 chore: Bump @librechat/agents to v3.2.44
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-06-21 08:39:10 -04:00
Danny Avila
5eb1c2c107
🖇️ feat: Reference Selected Chat Text with Multi-Quote Popup (#13868)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🖇️ feat: Reference Selected Chat Text with Multi-Quote Popup

Add a ChatGPT/Codex-style quote feature: selecting text in any message shows
an 'Add to chat' popup that accumulates removable quote chips above the
composer. On submit, the excerpts are merged into the user message text as
Markdown blockquotes (counted in the user message token count, not a system
message) and persisted on the message so they render on the user bubble and
survive reload.

- packages/api: add getReferencedQuotes + mergeQuotedText helpers (blockquote merge, length/count caps) with unit tests
- BaseClient.sendMessage: temporarily merge req.body.quotes into userMessage.text before buildMessages, restore clean text, persist quotes array
- data-schemas + data-provider: add optional quotes field to message schema/type
- client: pendingQuotesByConvoId atom, QuoteButton selection popup, PendingQuoteChips composer row, MessageQuotes persistent display
- useChatFunctions: drain pending quotes onto the message, carry forward on regenerate
- add localization keys and component/integration tests

* 🧪 test: Add Playwright e2e for chat quote feature

Add e2e/specs/mock/quotes.spec.ts covering select -> 'Add to chat' popup ->
chip -> send -> persistent reference block -> reload, plus multi-select
accumulation and chip removal. Selection is driven programmatically (real DOM
Range + dispatched mouseup) to summon the popup deterministically.

Add data-testid hooks (add-to-chat-button, pending-quote-chips, message-quotes)
to the quote components for stable selectors.

* 🛡️ fix: Address Codex review on quote feature

- Run PII filter + OpenAI moderation over req.body.quotes (P1): quoted excerpts
  are merged into the model-facing user message, so they must clear the same
  filters; a crafted quotes payload could otherwise bypass them. Adds tests.
- Carry quotes through edit/save-and-submit replays (overrideQuotes in
  EditMessage), mirroring overrideManualSkills, so edited turns keep context.
- Hide the quote UI for Assistants endpoints (which bypass BaseClient merge),
  so users can't queue quotes the assistant never receives.
- Clear pending quote/skill queues by resolved conversationId in useClearStates,
  not the UI index, so queued-but-unsent selections don't linger in Recoil.
- Cap queued quotes client-side at 10 to match the backend QUOTE_MAX_COUNT, so
  the composer never shows more quotes than are actually sent.

* 🧵 fix: Durably re-merge quotes + Codex round 2

Address Codex's re-review of the quote feature:

- Durable history re-merge (per maintainer decision): quotes are no longer
  merged at request time and stripped; instead each user message's persisted
  message.quotes is merged into its formatted content in AgentClient.buildMessages
  (new prependQuotes helper) for current AND historical turns. The model
  receives the referenced context on every prompt and the token count stays
  consistent with what was persisted; stored text stays clean for display.
- Attach normalized quotes to the user message in handleStartMethods (before
  getReqData/onStart) so the optimistic bubble, resumable abort metadata, and
  saved row all carry them (fixes the abort-metadata gap).
- Skip the quote drain entirely for Assistants endpoints in useChatFunctions,
  leaving the pending atom intact (UI is already hidden there).
- Normalize req.body.quotes via getReferencedQuotes before moderation/PII so
  only the trimmed/truncated/capped excerpts the model will receive are checked.
- Tests: prependQuotes unit tests; BaseClient quote tests assert early
  attachment + clean text; e2e now verifies the model receives the merged
  blockquote on the current turn and re-merged from history on a later turn
  (new E2E_ASSERT_QUOTE mock marker).

* 🔗 fix: Quote share/memo/abort/PII gaps (Codex round 3)

- Shared links: include quotes in the anonymized projection + SharedMessage
  type (+test) so the /share view renders the same reference blocks as the
  owner, mirroring manualSkills/alwaysAppliedSkills.
- MessageRender memo: compare quotes length so a server/resume copy whose only
  change is the quote list re-renders (the block no longer goes stale/missing).
- Resumable job metadata: include quotes in the userMessage written to
  GenerationJobManager so a reload/reconnect mid-stream reconstructs the chips.
- PII + moderation: also scan the merged blockquote+text exactly as the model
  receives it, so a secret split across a quote and the typed body (each clean
  alone) is caught (+cross-boundary test).
- e2e: make quote-add robust against the auto-scroll-dismisses-selection race
  via a retried select+click helper.

* 🛑 fix: Keep quotes on aborted turn's request message (Codex round 4)

abortMiddleware reconstructs finalEvent.requestMessage from jobData.userMessage
but only copied ids + text; include quotes so a stopped quoted turn keeps its
MessageQuotes in the UI and a regenerate-before-reload still sends the
referenced context. Completes the resumable-metadata fix from the prior round.

* 🧮 fix: Quote recount + preliminary abort metadata (Codex round 5)

- Force a canonical token recount for messages carrying quotes in
  AgentClient.buildMessages, so a plain text-only Save edit (which recomputes
  tokenCount from text alone) can't leave a stale, quote-excluding count that
  undercounts context on later turns — recount from the quote-merged copy
  self-heals it.
- Seed normalized quotes into the preliminary userMessage metadata
  (getPreliminaryUserMessage), so an abort during init/tool-loading (before
  onStart) still reconstructs the stopped turn's MessageQuotes.

*  fix: Add getReferencedQuotes to controller test mocks (CI)

request.js's getPreliminaryUserMessage now calls getReferencedQuotes; the
agents controller specs mock @librechat/api wholesale, so the mock must export
it or the call throws and cascades. Added a faithful mock (normalize/cap,
null when empty) to request.resumeMetadata.spec.js and jobReplacement.spec.js.

* 📐 fix: Quotes in context projection + resumable metadata (Codex round 6)

- Context-usage projection (resolveContextProjection): select message.quotes,
  prepend them into the projected user text, and recount quoted messages so the
  context gauge counts the same prompt the model receives (a text-only Save edit
  no longer makes the gauge undercount / over-report remaining budget).
- Resumable job metadata: trackUserMessage (created-event rewrite) and abortJob
  (final requestMessage) now carry quotes; SerializableJobData.userMessage and
  CreatedEvent.message gained an optional quotes field. With the cross-replica
  created-event spread, stopping/reconnecting a quoted turn after the created
  event keeps its MessageQuotes.

* 💬 feat: Collapse multi-select quotes into one chip with hover popup

Composer feedback: the quote chip area now shows a single chip — the excerpt
text for one selection, or a collapsed "{n} selections" pill for multiple,
with a hover popup (HoverCard) listing every excerpt and a per-item remove. The
chip is taller (py-1.5/text-sm) to read less skinny. Adds com_ui_quote_selections
and com_ui_remove_all_quotes; updates unit + e2e tests (e2e drives the count via
a data-quote-count hook and exercises the hover popup).

*  fix: Make multi-selection quote popup keyboard accessible

The collapsed "{n} selections" pill used a HoverCard, which Radix only opens on
pointer hover — its interactive content was unreachable by keyboard. Replaced it
with a Popover: the trigger is a real button that opens on click / Enter / Space
(focus moves into the list, each excerpt's × is tab-navigable, Escape closes and
restores focus), with hover-open preserved for mouse via controlled open state +
a close grace period. Hover-initiated opens skip auto-focus so they don't pull
focus off the composer. Adds an e2e asserting keyboard open/close.

* 📐 fix: Clamp the Add-to-chat button within the viewport (Codex round 7)

The floating selection button positioned via translate(-50%,-100%) (bottom-center
anchor) but clamped top/left as if they were its top-left, so a selection near
the viewport top or sides could render the button partly/fully offscreen. Now it
measures the button (ref + useLayoutEffect) and computes an on-screen top-left —
clamping by the full width within side margins and flipping below the selection
when there's no room above — with no transform, and stays hidden until measured
so it never flashes at an unclamped spot.

* ↩️ fix: Restore pending quotes on early-abort draft (Codex round 8)

When a turn is stopped before the created event (e.g. during tool/MCP init), the
final handler restores requestMessage.text to the draft, but the pending-quote
atom was already drained on submit — so a retry sent no quotes. The abort
requestMessage now carries quotes (preliminary metadata + abort fixes), so the
three early-abort/no-response draft-restore paths in useEventHandlers now also
re-queue pendingQuotesByConvoId from requestMessage.quotes.

*  fix: Use Ariakit Popover for quote selections (keyboard focus)

The multi-selection popup used a hand-rolled Radix Popover with Popover.Anchor +
a manual button, so Radix had no trigger to return focus to — Escape dumped
focus to the page top. Refactored to Ariakit (the codebase's popover primitive,
per DropdownPopup/Fork): the `PopoverDisclosure` is the real trigger, so Escape
closes and returns focus to the composer instead of the top of the page. Keyboard
opens (Enter/Space) autofocus into the list and tab through each excerpt's remove;
hover opens for mouse with autofocus suppressed so it never pulls focus off the
composer. e2e asserts the keyboard open/navigate/Escape flow keeps focus on a
real control (never BODY).
2026-06-21 08:33:11 -04:00
Danny Avila
e515063ffe
🔗 feat: Snapshot Files for Shared-Link Attachments (#13740)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🔗 feat: Snapshot Files for Shared-Link Attachments

Shared-link viewers could read a shared conversation snapshot but not its
attachments: file preview/download still went through the owner-scoped file
ACL (the /api/files router sits behind requireJwtAuth + owner/agent checks),
so anonymous viewers got 401s and authenticated non-owners got 403s — the
repeated `[fileAccess] denied` warnings seen for the preview poller.

Capture an immutable per-share file snapshot (embedded on the SharedLink
document, referencing the original stored object — no byte copy) at share
create/update, and serve those files through new share-scoped routes
authorized by the existing shared-link view permission (public/ACL) plus
snapshot membership, never the owner's live file ACL.

- data-schemas: fileSnapshots on the share doc; capture in create/update;
  read-time rewrite of filepath/preview to /api/share/:id/files/:fileId;
  getSharedLinkFile + lazy backfillSharedLinkFiles for legacy links
- api: GET /api/share/:shareId/files/:file_id[/download|/preview]; route
  context added to fileAccess denial logs
- packages/api: isFileSnapshotEnabled resolver (env + yaml)
- data-provider: interface.sharedLinks.snapshotFiles (default on) + client
  endpoints/services
- client: ShareContext.shareId wired to Image, preview hook, and downloads
- config: SHARED_LINKS_SNAPSHOT_FILES env override (default on)

* 🔒 fix: Address Codex review on shared-link file snapshots

Triage of the Codex review on PR #13740 (2 P1, 7 P2 — all valid):

- P1 (cross-user access): scope the snapshot lookup to the sharing user's own
  files so a message referencing another user's file_id can't widen access.
- P1 (stored XSS): the inline share-file route now serves only safe preview
  types inline (raster images/pdf); everything else is forced to attachment with
  X-Content-Type-Options: nosniff.
- Stream shared downloads by default; redirect to a signed URL only on
  ?direct=true (blob/XHR callers work without bucket CORS).
- Read preview status live from the file record (always current for deferred
  previews) and stop embedding extracted text in the share doc (16MB-limit risk).
- Only lazily backfill when the fileSnapshots field is absent (legacy), not on
  every snapshot miss.
- Backfill legacy shares before rewriting message URLs, and gate URL rewriting
  to public shares so non-public (ACL) shares keep prior behavior (img/anchor
  can't carry the bearer token).
- Frontend: only route a download through the share path when the file was
  actually snapshotted (rewritten href / filepath), else fall back.

* 🔑 feat: Authorize shared-link files for non-public shares via cookie

Extends shared-link file access to non-public (ACL) shares (Codex finding 5).
`<img>`/anchor requests can't carry the bearer access token, so non-public
shares previously 401'd on file loads. Add an optional cookie-auth fallback on
the share file routes that resolves the viewer from the `refreshToken` cookie
(or signed `openid_user_id` cookie) — the same mechanism secure image links use
(validateImageRequest) — then let canAccessSharedLink run the viewer's ACL check.

- new middleware optionalShareFileAuth (+ unit spec); applied to the three
  share file routes after optionalJwtAuth
- URL rewriting in getSharedMessages is no longer gated to public shares (the
  route now authorizes header-less requests), so files work uniformly across
  public and non-public shares; revert the now-unused req.sharePublic plumbing

* 🔒 fix: Second Codex pass on shared-link file snapshots

Addresses the follow-up Codex findings on PR #13740:

- Don't snapshot transient text-source files: FileSources.text filepaths are
  Multer temp paths the upload route deletes, so they can't be streamed —
  removed from the streamable allowlist.
- Unset stale snapshots on a disabled-feature update: updateSharedLink now
  $unsets fileSnapshots when snapshotFiles is false, so an opted-out update
  can't keep serving file ids the update dropped.
- Load tenant config after share resolution: configMiddleware now runs after
  canAccessSharedLink (which enters the share's tenant ALS context), so
  per-tenant interface.sharedLinks.snapshotFiles overrides apply to anonymous
  public views.
- Return a clean 404 when the snapshotted object is gone: resolveShareFile now
  requires the live file record and 404s if it's been deleted/expired, instead
  of letting the stream error after headers are sent (ENOENT / 500).

(The re-flagged P1 about private-viewer rewriting was already fixed in the prior
commit's cookie-auth change.)

* 🔒 fix: Third Codex pass on shared-link file snapshots

Addresses the third Codex review pass on PR #13740:

- P1: keep shared previews/files pinned to the snapshotted version. Snapshot the
  small previewRevision; resolveShareFile 404s when the live file's revision no
  longer matches (file_id reused/overwritten by a later turn), so old links can't
  surface post-share content — covers both preview text and streamed bytes.
- Honor the toggle as a kill switch: resolveShareFile 404s when snapshotFiles is
  disabled, instead of only skipping backfill, so disabling stops serving
  already-snapshotted file URLs.
- Lazy-sweep orphaned 'pending' previews to 'failed' in the share preview route
  (mirrors the owner route) so the client poller reaches a terminal state.
- Resolve the cookie-fallback user in runAsSystem so strict tenant isolation
  doesn't throw before canAccessSharedLink establishes the share tenant context.

*  feat: Per-link "share files" checkbox for shared links

Add a checkbox to the share-link dialog (checked by default) letting the user
choose whether to include the conversation's files in the shared link, with
copy explaining images/files won't be visible to viewers otherwise. Opting out
skips snapshot creation/serving for that link.

- client: ShareButton renders the checkbox gated on the new
  startupConfig.sharedLinksSnapshotFilesEnabled flag; state threads through
  SharedLinkButton into the create/update mutations as `snapshotFiles`.
- data-provider: createSharedLink/updateSharedLink send `snapshotFiles` in the
  body; TStartupConfig gains `sharedLinksSnapshotFilesEnabled`.
- api: POST/PATCH /api/share compute snapshotFiles as
  isFileSnapshotEnabled(req.config) && body.snapshotFiles !== false (admin gate
  AND per-link opt-out); config.js exposes the effective enabled flag to clients.
- en locale: com_ui_share_files (+ _description).

* 🐛 fix: Make the "share files" opt-out actually hide files

Unchecking "share files" at creation didn't hide anything: the shared message
JSON still carried each file's original (e.g. static-served) path, and because
opting out only meant "no fileSnapshots field" — indistinguishable from a legacy
link — getSharedMessages would backfill snapshots on first view whenever the
admin feature was on, re-enabling files entirely.

Fix by persisting and honoring the per-link choice:
- Store `snapshotFiles` (boolean) on the SharedLink so opt-out is distinct from a
  legacy link; set it on create and update.
- getSharedMessages computes includeFiles = adminEnabled && link not opted out;
  when excluded it strips files/attachments from the payload (no original-path
  leak) and never backfills the opted-out link.
- Surface the stored choice via getSharedLink so the dialog checkbox reflects an
  existing link's actual setting instead of always defaulting to checked.

Note: changing the checkbox on an already-created link still applies only when
the link is refreshed (which regenerates the URL) — a UX follow-up.

* 🔒 fix: Close remaining shared-link file opt-out leaks (Codex)

Follow-up to the per-link opt-out, addressing the third Codex pass:

- Honor the opt-out on the file route too: getSharedLinkFile now returns the
  link's `optedOut` choice; resolveShareFile 404s (and never backfills) an
  opted-out link, so a direct /files/:id request can't re-create snapshots.
- Make read/serve viewer-independent: the gate no longer uses the viewer's
  resolved config (isFileSnapshotEnabled(req.config)) — it uses the link's stored
  choice plus a global env-only kill switch (isFileSnapshotKillSwitchActive). A
  viewer's own interface.sharedLinks.snapshotFiles can no longer hide a link's
  files. Create/update still use the creator's config to set the per-link choice.
- Neutralize render URLs for non-snapshotted files: applyShareFileRoute now
  strips filepath/preview for any file/attachment not in the snapshot, so the
  owner's original (e.g. static) path can't be loaded through the share.

* 🔒 fix: Harden shared-file version pinning and local path handling (Codex)

- Refuse reused/overwritten file snapshots more broadly: resolveShareFile now
  refuses to serve when either previewRevision OR `bytes` changed vs the
  snapshot. `bytes` catches non-office reused outputs (e.g. code-exec
  same-filename images that lack previewRevision) and is stable across S3 URL
  refresh and the pending->ready transition. Same-size content swaps remain a
  best-effort gap inherent to the no-byte-copy design.
- Strip cache-busting query strings before local streaming: code-output images
  add `?v=...` to filepath; the share route now splits it off so getLocalFileStream
  resolves the real filename instead of a literal `*.png?v=...` path.

* 💬 fix: Clarify that file-sharing changes apply on link refresh

For an already-created shared link, changing the "share files" checkbox only
takes effect when the link is refreshed (which regenerates the snapshot). Add a
note under the checkbox, shown only when a link already exists, so the behavior
isn't surprising: "Refresh the link to apply this change — files are snapshotted
when the link is refreshed."
2026-06-20 23:05:13 -04:00
Matheus Serpa
21d98b85bd
🏷️ fix: Scope File Search entity_id to Agent Knowledge-Base Files Only (#13693)
User-attached files are embedded by the RAG API under the user id (no
entity), while only agent knowledge-base files are embedded under the
agent's entity_id. Sending entity_id in every /query request made the
RAG API's entity filter return no results for user attachments — with a
shared agent, files attached to the message were effectively invisible
to the file_search tool, while knowledge-base files kept working (which
masked the bug).

primeFiles now tags each file with fromAgent (whether it belongs to the
agent's file_search.file_ids) and createQueryBody only includes
entity_id when fromAgent === true — the safe default for callers that
omit the flag is to query without entity scoping. Tests cover KB files,
user attachments, the omitted-flag default, and restore RAG_API_URL.
2026-06-20 10:18:25 -04:00
Airam Hernández Hernández
3926fda234
🎒 fix: Apply OCR Context to Responses API Agents and Handoffs (#13707) 2026-06-20 10:17:09 -04:00
Danny Avila
59637e136f
📦 chore: Bump @librechat/agents to v3.2.43 (#13854)
Some checks failed
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (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 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
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
2026-06-19 16:20:27 -04:00
Danny Avila
91f25b8302
📦 chore: bump @librechat/agents to v3.2.42 (#13848)
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (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
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🔧 chore: Update dependencies in package-lock.json and package.json

Bump `form-data` to version 4.0.6 and update `hasown` and `mime-types` dependencies in package-lock.json. Add an `overrides` section in package.json to ensure compatibility with the new `form-data` version.

* 📦 chore: Bump `@librechat/agents` to v3.2.42
2026-06-19 09:47:46 -04:00
Dustin Healy
6a63531eb4
📒 feat: Audit Log Backend for SystemGrant Assign and Revoke Events (#13087)
* 🛡️ feat: Audit log backend for SystemGrants changes

Add an AuditLog Mongoose collection that records every grant assign/revoke as an append-only entry capturing the actor, target principal, capability, timestamp, and tenant scope. Wire the entry-write into the existing admin assignGrant and revokeGrant handlers so the admin panel's audit-log tab populates as grants happen.

The data-schemas package gains the IAuditLog type, a Mongoose schema with tenant + target compound indexes for keyset pagination, a model factory wired through createModels, and an AuditLog methods factory exposing recordAuditEntry, listAuditLogPage (cursor-paginated, faceted, search-aware), findAuditLogEntry, and streamAuditLogEntries.

The packages/api admin layer adds createAdminAuditLogHandlers with three handlers backing the routes the admin panel already consumes: GET /api/admin/audit-log returns paginated entries, GET /api/admin/audit-log/:id returns a single entry for the permalink drawer, and GET /api/admin/audit-log/export.csv streams CSV with formula-injection defang plus UTF-8 BOM.

The Express layer mounts the new router at /api/admin/audit-log behind requireJwtAuth and the ACCESS_ADMIN capability, matching the existing admin route pattern. The audit emission failure is logged via logger.error but never rolls back the grant.

* 🧹 chore: Audit log backend cleanup — offset pagination, name-based filters, type tightening

Switch listAuditLogPage from cursor-based to offset-based pagination with skip().limit() + parallel countDocuments, returning { entries, total } instead of { entries, nextCursor }; the cursor encode and decode helpers are no longer needed and have been removed.

Interpret the actorId and targetPrincipalId filter parameters as case-insensitive partial regex against the denormalized actorName and targetName fields rather than exact-match against the underlying ObjectId. Admin panel users naturally filter by human name, not by Mongo identifier.

Replace the broad Record<string, unknown> casts on req.query with a typed AuditLogQuery shape, drop two unused exported types and the now-unused mongoose Types import, and fix the streamAuditLogEntries Omit literal to match the interface and the offset-based design.

* 🛠️ fix: Address audit log review feedback (CI typecheck, ISO offsets, no-op revoke, deps surface, schema, backpressure, tests)

Resolve the duplicate AuditAction export that broke the data-schemas TypeScript check by importing the canonical declaration from types/admin instead of re-declaring it in types/auditLog.

Accept timezone-offset ISO 8601 timestamps such as 2026-05-01T09:30:00+02:00 in the from and to filter params and reject local-time strings without a zone so every request resolves to an unambiguous instant.

Skip the audit emission on no-op revokes: revokeCapability now returns deletedCount so the admin handler can omit the grant_removed entry when the target grant did not exist, keeping the audit trail factually accurate. Mocks in the existing grants.spec.ts updated to the new return shape.

Drop the required recordAuditEntry from AdminAuditLogDeps since the audit-log handler factory never consumes it; the grants handler factory keeps its optional dep for the write path.

Tighten the tenantId validator on the audit log schema to require a non-empty trimmed string, and rewrite the listing-index comment to describe deterministic offset sort instead of keyset pagination.

Stream the CSV export with explicit backpressure (await drain when res.write returns false) and abort on client disconnect so a cancelled download no longer pins a Mongo cursor or buffers unbounded data in memory.

Add packages/data-schemas/src/methods/auditLog.spec.ts covering tenant and platform scoping, single and multi action filtering, partial-name filtering for actor and capability, the createdAt window, offset pagination with total, ObjectId and date stringification on the wire, regex-metacharacter escape, and streaming completeness.

* 🛠️ fix: Address P1 audit-log review findings (cursor cancel, drain race, filter naming, type dedupe, tenant scope, log enrichment)

The CSV stream handler kept draining Mongo batches after the client
disconnected because the `for await` loop only honored its abort flag
inside `onEntry`. Thread an `isCancelled` callback into
`streamAuditLogEntries` so the methods layer closes the cursor as soon
as the handler sees `close`/`aborted`; a `finally` block guarantees
release on throw. The drain promise in `writeChunk` now races against
the response's `close` event so a destroyed socket cannot strand the
handler on a `drain` that will never fire.

The HTTP filter keys `actorId` and `targetPrincipalId` always did
case-insensitive substring matches on the denormalized `actorName` /
`targetName` columns, never on ObjectIds — a client passing a real id
silently got zero rows. Renamed the wire-level keys to `actorQuery` /
`targetQuery` (matching what the matcher actually does) and kept the
old names as deprecated aliases for one release so the sibling
admin-panel PR can migrate without breaking; each legacy use logs a
deprecation warning. Renamed the corresponding fields in
`AuditLogFilters` too.

`AdminAuditLogEntryWire` duplicated `AdminAuditLogEntry` from
`types/admin.ts` field-for-field, violating the no-duplicate-types
rule. Deleted the duplicate, hoisted `AuditLogPage`,
`RecordAuditEntryInput`, and `AuditLogFilters` from
`methods/auditLog.ts` into `types/auditLog.ts`, and updated the
handler, method factory, and re-exports accordingly.

`tenantFilter` treated `''` as a valid tenant scope, producing a
`{ tenantId: '' }` query that silently returned nothing while the
schema validator rejected `''` on writes. Switched to a strict
`typeof tenantId === 'string' && tenantId.trim().length > 0` check so
reads agree with writes, with new spec coverage for empty and
whitespace-only inputs.

Audit-write failures now log the full forensic payload (action,
capability, tenantId, actorId, target metadata) inside a single meta
object so winston's standard signature surfaces it correctly; a comment
on the catch block explains why the failure mode stays silent (it must
never block a privileged operation).

Stronger filter parsing: invalid `action` values and unknown
`targetPrincipalType` now return 400 instead of silently dropping.
Extracted `MAX_LIMIT` to a constant. Replaced the
`Record<string, Date>` cast in `buildFilter` with a typed local.
Switched the stream cursor to `lean<IAuditLog[]>()` and removed the
`as IAuditLog` cast inside the loop.

*  test: Cover admin audit-log handler with unit tests for auth, validation, tenant isolation, CSV output, and abort

The sibling admin handlers (grants, groups, roles, users) all have
handler specs; this one was missing. The new suite covers 401 on a
missing `req.user`, 400 on malformed ISO `from` / `to`, 400 on
limit > 500, 400 on negative offset, 400 on an unknown action or
`targetPrincipalType`, 400 on a non-ObjectId `:id`, 404 when the
methods layer returns null, that the caller's `tenantId` (not a
forged query-string `tenantId`) is the one passed to the methods
layer, that `actorQuery` / `targetQuery` round-trip, that the
deprecated `actorId` / `targetPrincipalId` aliases still map through,
that the CSV stream emits the BOM as the first chunk with CRLF line
endings and the expected header labels, that quotes, commas, and
newlines are properly escaped, that the formula-injection prefixes
(`=` `+` `-` `@` tab CR) are defanged, that an `isCancelled` callback
reaches the methods layer and flips to true on client `close`, and
that `res.end` is skipped when the client disconnected mid-stream.

* 🛡️ feat: Enforce append-only AuditLog at the schema level

Every field is now marked `immutable: true`, and pre-hooks on the
schema reject `updateOne`, `updateMany`, `findOneAndUpdate`,
`findOneAndReplace`, `replaceOne`, `deleteOne`, `deleteMany`,
`findOneAndDelete`, plus any `save()` against an existing document.
`timestamps` is reduced to `{ createdAt: true, updatedAt: false }`
since a mutable timestamp would imply mutation is allowed, and
`updatedAt` is dropped from `AuditLog` / `IAuditLog`. The methods
spec resets state between tests via the raw driver (`AuditLog.collection.deleteMany`),
which bypasses the pre-hooks; new specs assert that the model-level
update / delete / re-save paths reject with the append-only error and
that `updatedAt` is not stamped on new documents.

* ♻️ refactor: Share MAX_AUDIT_LOG_LIMIT between methods and handler

Renamed the methods-layer constant from the generic `MAX_LIMIT` to
`MAX_AUDIT_LOG_LIMIT`, exported it through `@librechat/data-schemas`,
and consumed it from the handler instead of duplicating `500` there.
Now the limit is single-sourced; bumping it once updates both the
clamp inside `listAuditLogPage` and the 400-error boundary the
handler returns to clients.

* 🛡️ feat: Gate audit-log routes on a dedicated `READ_AUDIT_LOG` capability

The audit-log routes were gated on `ACCESS_ADMIN`, which conflates "can log
into the admin panel" with "can see who granted what to whom." Anyone with
`ACCESS_ADMIN + READ_CONFIGS` (a config reviewer with no people-management
authority) could read the grant history of every user, group, and role —
information they have no need to know.

`READ_AUDIT_LOG` ('read:audit_log') is now an explicit, separately grantable
read capability with no MANAGE counterpart, matching the append-only nature
of the collection. `seedSystemGrants` iterates `Object.values(SystemCapabilities)`
so existing ADMIN-role seeds pick it up automatically on next startup.

This also makes an "auditor" persona possible: hold `ACCESS_ADMIN + READ_AUDIT_LOG`
without any MANAGE_* grants and you can review history without modifying anything.

* ♻️ refactor: Share AUDIT_ACTIONS, tighten audit dep types, document route order

Exports a runtime AUDIT_ACTIONS array from packages/data-schemas alongside the
AuditAction type so the Mongoose schema enum and the HTTP handler's whitelist
consume one source of truth instead of duplicating the literal pair.

Switches the grants handler's recordAuditEntry dep typing from a duplicated
inline object literal returning Promise<unknown> to the published
RecordAuditEntryInput type returning Promise<void>, and tightens the local
emitAudit args to AuditAction. Replaces the local ParsedFilters interface in
the audit-log handler with Omit<AuditLogFilters, 'offset' | 'limit'> to drop
the duplicate definition.

Drops the optional marker on AuditLog.createdAt. Mongoose always sets it at
insert time, so callers treating it as nullable were guarding against a state
the schema does not produce.

Adds a comment on api/server/routes/admin/audit.js noting that /export.csv
must precede /:id so a future contributor does not accidentally reorder them
into a 404 trap.

* 🛡️ feat: Resolve audit names without extra DB round-trips

For the actor name, JWT-authenticated `req.user` already carries `name`,
`username`, and `email`. `resolveUser` now derives the actor display name
from `req.user` directly and threads it through the caller context, so
every grant assign and revoke no longer triggers a separate `getUserById`
lookup.

For the target name, replaces the previous always-store-the-principalId
behavior (which buried opaque ObjectId strings in immutable audit rows
for USER and GROUP targets) with a `resolveTargetName` dep. ROLE
principals continue to use `principalId` directly because the SystemGrant
model stores role names there. USER and GROUP principals route through
the new dep, which in `api/server/routes/admin/grants.js` calls
`db.getUserById` or `db.findGroupById` respectively and falls back to
the principalId on miss or error so the audit row stays intelligible.

Drops the misleading "display name lookup happens in a later iteration"
comment.

*  test: Cover audit emission, scope emitAudit to today's ROLE-only surface

Fixes a misleading test that claimed to verify "idempotent even if the grant
does not exist" while mocking deletedCount: 1 (the grant DID exist). Replaces
it with the actual no-op scenario (deletedCount: 0) and adds an assertion
that recordAuditEntry is NOT called, since the whole point of the
deletedCount > 0 gate is to avoid fictitious revocation rows.

Adds a dedicated audit emission describe block covering: grant_assigned
emission with the actor name resolved from req.user, grant_removed
emission when deletedCount is positive, and the no-emission fallback when
recordAuditEntry is not configured. The actor-name assertions exercise the
name / username / email fallback chain in resolveUser.

The previous commit also added a `resolveTargetName` dep and an
emitAudit branch for USER/GROUP targets. The grants surface is ROLE-only
today (MANAGE_CAPABILITY_BY_TYPE has only PrincipalType.ROLE), so that
code path is unreachable from the handler. Removed the dep and the
branch; the audit row uses principalId as the target name, which is the
human-readable role name for ROLE principals. A comment in emitAudit
flags where to plumb resolveTargetName back in once USER and GROUP
grants are enabled.

* 🛠️ fix: Inclusive `to` date filter and reject inverted ranges

A `?to=2025-01-15` filter previously stopped at midnight UTC of that
day, silently excluding everything that happened on January 15. The
`parseIsoDate` helper now widens a bare `YYYY-MM-DD` to 23:59:59.999Z
when called with the `end` boundary. Full ISO timestamps are honored
exactly, so callers that want minute-precision can still get it.

Also rejects inverted ranges (`from` later than `to`) with a 400 so
operators see a clear error instead of a silent empty result.

* 🛡️ feat: Cap audit-log CSV exports at 100k rows; cover stream error path

Introduces MAX_AUDIT_EXPORT_ROWS (100k) and threads a `maxRows` option
through streamAuditLogEntries. The handler now passes the cap into the
stream so a careless admin script or a hostile auditor cannot pin a
Node worker and a Mongo cursor by exporting unbounded result sets.
Beyond 100k rows, callers should slice exports by from / to date.

Adds a methods-layer spec for the cap behavior, a handler-layer spec
that asserts the option is plumbed through, and a handler-layer spec
that exercises the streamAuditLogEntries-throws-after-headers-sent path
(catch block falls through to res.end instead of attempting JSON).

Documents on buildFilter that case-insensitive substring regex filters
(actorName, targetName, capability, search) cannot use a B-tree index
and degrade to a tenant-scoped partition scan, so deployments with
hundreds of thousands of audit rows per tenant should constrain those
queries with a date window.

* 🧹 chore: Spell CSV_BOM as  and drop a gratuitous optional chain

`revokeCapability` is typed `Promise<{ deletedCount: number }>` so the
`?.` on `revokeResult?.deletedCount` only obscured that the value cannot
be nullish.

`CSV_BOM` was a literal U+FEFF character invisible in most editors. Now
spelled as the Unicode escape so readers can see the constant; the test
that asserts on the first emitted chunk uses the same escape.

* 🔧 chore: Allowlist AuditLog in the tenant-isolation coverage guard

The AuditLog collection carries a tenantId field but scopes tenancy manually
inside listAuditLogPage / streamAuditLogEntries / recordAuditEntry using the
same $exists: false convention as SystemGrant. The tenant-isolation plugin
coverage spec now allows that and asserts it stays accurate.

* 🛠️ fix: Normalize blank tenantId before persisting audit entries

The `recordAuditEntry` write path was treating any non-null tenantId as a
real string, so empty or whitespace-only values reached the schema validator,
failed the non-empty-string check, and silently dropped the audit row. The
read-side `tenantFilter` already treats those values as platform-level scope,
so the write path now mirrors it: blank or whitespace-only tenantId becomes
an omitted field, which matches `{ tenantId: { $exists: false } }` queries
and clears validation. Added a regression test that records two entries with
blank and whitespace tenantId and asserts both persist with the tenantId
field absent.

* 🎨 style: collapse expect.objectContaining onto one line to satisfy prettier

* 🔒 fix: block document-level deleteOne/updateOne on AuditLog

Mongoose registers deleteOne and updateOne pre-hooks as query middleware
by default. The query-level append-only block on AuditLog therefore did
not cover Document.prototype.deleteOne() or Document.prototype.updateOne(),
leaving a path where a caller that had already loaded an audit row via
findOne could call .deleteOne() or .updateOne() on the instance and bypass
the schema contract.

Explicit { document: true, query: false } registrations close the holes,
and the spec now covers both code paths against a real in-memory Mongo.

* 🔒 fix: require ACCESS_ADMIN on audit-log routes

Every other admin router (config, grants, users, roles, groups, auth)
enforces requireJwtAuth followed by requireCapability(ACCESS_ADMIN) before
any feature-specific capability check. The audit-log router only required
READ_AUDIT_LOG, which is independent of ACCESS_ADMIN in CapabilityImplications,
so a role delegated only READ_AUDIT_LOG without ACCESS_ADMIN could read or
CSV-export the audit trail and bypass the admin boundary.

Aligned the middleware chain with the rest of the admin surface so
ACCESS_ADMIN gates entry and READ_AUDIT_LOG gates the feature within it.

* 🎨 chore: re-sort imports after dev rebase

Post-rebase sort-imports against the merge target — six audit-log files
landed with stale import ordering relative to the current scripts/sort-imports.mts
rules on dev. CI's import-order job flagged the drift; running the script
locally rewrites them in place. No semantic changes.

* 🔧 fix: explicit type annotations on audit-log model + schema exports

Dev migrated packages/data-schemas builds from rollup to tsdown with
--isolatedDeclarations enabled, which requires every exported function to
declare its return type and every exported variable to declare its type.
Two of our audit-log exports got swept up:

  TS9007 models/auditLog.ts:12  createAuditLogModel return type
  TS9010 schema/auditLog.ts:12  auditLogSchema variable type

Added Model<t.IAuditLog> on the factory and Schema<IAuditLog> on the
schema variable, matching the sibling SystemGrant convention. No runtime
behavior change.

* 🔧 fix: align revokeCapability type annotation with implementation

The rebase auto-merge of systemGrant.ts kept dev's outer type annotation
(`revokeCapability: ... => Promise<void>`) but our implementation returns
`Promise<{ deletedCount: number }>` (added during the bot-review loop to
let the audit emitter distinguish a real revoke from a no-op against a
nonexistent grant). The mismatch surfaced as TS2719 on the methods record
return at line 520. Updated the type annotation to match the impl.

The caller at packages/api/src/admin/grants.ts:444 reads
`revokeResult.deletedCount` to gate the audit emit, so the wider return
type is what the rest of the code already assumes.

* 🔧 fix: explicit factory return type on createAdminAuditLogHandlers

Same tsdown --isolatedDeclarations migration that hit packages/data-schemas
also applies to packages/api; the audit-log handler factory's inferred
return type tripped TS9013 against the new build pipeline. Annotated the
factory with explicit handler signatures matching the sibling
createAdminGrantsHandlers convention. Used Promise<Response | void> for
the export handler because its final res.end() path returns undefined,
unlike the other two handlers which always return a Response.

* 🛡️ feat: Generalize audit log into a tamper-evident, extensible event substrate

Reworks the SystemGrant-only audit log into a general-purpose, append-only
compliance substrate designed to absorb future event classes (agent runs,
tool/MCP calls, config + permission changes, approvals) without reshaping the
record. Nothing was shipped yet, so this replaces the grant-specific wire
shape rather than layering aliases.

Schema / record shape (packages/data-schemas):
- schemaVersion + two-level taxonomy: category + namespaced action
  (grant.assigned/grant.removed), first-class outcome and severity.
- Structured actor{type,id,name} supporting non-user actors (system, agent,
  service, schedule, webhook, api); generic target{type,id,name}; open
  metadata map; request context{requestId,ip,userAgent,sessionId}.

Tamper-evidence (hash chain):
- Per-tenant chain keyed by chainKey with seq/prevHash/hash. Appends link to
  the previous hash; a unique {chainKey,seq} index serializes concurrent
  writes (dup-key retry) so the chain can never fork. createdAt is explicit so
  it's covered by the hash.
- verifyAuditChain() walks a chain and detects modification, deletion, and
  forged links; exposed via GET /api/admin/audit-log/verify.

Other best-practice gaps from the review:
- Keyset (cursor) pagination over seq alongside offset; stable under
  concurrent appends. nextCursor in the page payload.
- Retention: purgeAuditLogEntries() privileged prefix-purge with a confirm
  latch, returns a checkpoint; verify tolerates a purged prefix.
- Fail-closed option (AUDIT_LOG_FAIL_CLOSED) so a failed audit write can fail
  the grant request instead of being swallowed; default stays fail-open.
- Grant handlers now capture request context and emit the new shape.

CSV export updated for the new columns (incl. seq/hash). data-schemas bumped
to 0.0.54 for the sibling admin-panel consumer. Tests rewritten: 28
methods-layer cases (chain genesis/linking, tamper detection, keyset, purge)
and the handler/grants specs updated for the new shape, fail-closed, and the
verify endpoint.

* 🛠️ fix: Address Codex review on the audit-log substrate

- F1 (fail-closed atomicity): assign/revoke now compensate (rollback grant /
  restore grant) when a fail-closed audit write fails, so a 5xx never leaves an
  unaudited mutation.
- F5: only emit grant.assigned for a real change — skip the audit when the role
  already holds the capability (idempotent re-assert).
- F7: verifyAuditChain no longer silently trusts a non-genesis start; a purged
  prefix must be authorized by a trusted checkpoint (purge now returns
  {throughSeq, prevHash}), else verification fails as tampering.
- F4: block Model.bulkWrite on AuditLog (would bypass the append-only middleware).
- F3: CSV export appends an explicit TRUNCATED marker + logs when the row cap is hit.
- F6: reject out-of-range date-only filters (2025-02-31) instead of normalizing.
- F2: regenerate package-lock.json for the 0.0.54 data-schemas bump.

Tests: +1 methods (bulkWrite) +2 verify (deleted-prefix / checkpoint mismatch),
updated purge test for checkpoint flow; +4 api (re-assert skip, assign/revoke
fail-closed rollback, date reject, CSV truncation marker).

* 🛠️ fix: Address Codex round-2 on the audit-log substrate

- R2-1/R2-5 (P1/P2): base the grant.assigned audit decision on the atomic
  upsert result. grantCapability now returns { grant, created } via
  includeResultMetadata; the handler audits only when created. Removes the racy
  pre-read, which also mis-handled inherited platform grants vs a new
  tenant-scoped insert and concurrent double-assign.
- R2-2 (P2): namespace tenant chain keys (tenant:<id>) so a tenant whose id is
  literally the platform sentinel can't share the platform audit chain.
- R2-4 (P2): validate literal calendar tokens for full ISO timestamps too, so
  2025-02-31T00:00:00Z is rejected instead of normalizing to March 3.

Tests updated for the grantCapability { grant, created } contract (systemGrant +
grants specs) and the namespaced chain key (auditChainKey helper); +1 api date
case. data-schemas 141, api grants/audit 107 green.

R2-3 (deprecated actorId/targetPrincipalId aliases): not reinstating — the
surface is pre-release and its only consumer (admin-panel PR) migrates to the new
shape in lockstep, so there are no legacy clients to support.
R2-6 (role-deletion cascade emits no grant.removed): valid but a separate
workflow in roles.ts; tracked as a follow-up to keep this PR scoped.

* 🛠️ fix: Address Codex round-3 on the audit-log substrate

- R3-3 (P2): make a grant re-assert a true no-op — move grantedAt/grantedBy to
  $setOnInsert so an existing grant is never silently mutated when the audit is
  skipped (created:false now means nothing changed). grantedAt/grantedBy record
  the original grant.
- R3-2 (P2): report CSV export truncation exactly. streamAuditLogEntries returns
  { count, truncated }; truncated is true only when rows existed beyond the cap,
  so an exact-cap export is no longer falsely marked truncated.
- R3-5 (P2): block AuditLog.insertMany (another bulk path that skips the save
  hook and could inject forged seq/prevHash/hash and poison the chain).

Tests: +insertMany rejection, +exact-cap vs truncated stream cases, +exact-cap
export-not-truncated handler case. ds 142, api 108 green.

R3-1 (deprecated query aliases) and R3-4 (role-deletion cascade audit) are
re-flags of R2-3/R2-6 — holding the prior decisions (pre-release surface; separate
roles.ts workflow tracked as a follow-up), pending maintainer direction.

* 🛡️ feat: Audit grant removals from the role-deletion cascade

Closes the forensic gap Codex flagged (R2-6/R3-4): deleting a role removed its
SystemGrants with no audit entries. `deleteGrantsForPrincipal` now returns the
removed grants, and the role-deletion handler emits a `grant.removed` audit entry
per removed grant (actor = caller, target = role, metadata.capability, request
context), matching the explicit revoke endpoint. Fail-open — the role is already
deleted, so a failed audit is logged, not propagated; sequential to keep the
per-tenant hash chain ordered.

Extracted `buildAuditContext` to admin/context.ts (shared by grants + roles).
Tests: role-deletion emits one entry per grant / none when no grants; ds 110,
api admin 202 green.

* 🛠️ fix: Address Codex round-4 on the audit-log substrate

- R4-1 (P2): don't silently drop an audit row under heavy append contention.
  recordAuditEntry now retries duplicate-key seq collisions up to 12× with
  jittered backoff (was 5, no backoff), so realistic bursts of parallel admin
  writes resolve; the failClosed escape still applies on true exhaustion.
- R4-3 (P2): purge a contiguous seq prefix, not a date range. createdAt is
  app-generated, so under multi-instance clock skew a later seq can carry an
  earlier timestamp; a raw date delete could remove an interior row and break
  verification. purgeAuditLogEntries now resolves the date to the first retained
  seq and deletes only strictly-lower seqs, keeping the remaining chain contiguous.

Tests: +clock-skew purge case (no gap created). ds auditLog 33 green.

R4-2 (role-deletion grant audit) is a re-flag of R2-6/R3-4, already implemented
in 15472127d6 (roles.ts emitGrantRemovals + route wiring + tests); the finding's
cited line numbers predate that commit.

* 🛠️ fix: Address Codex round-5 on the audit-log substrate

- R5-1 (P2): scope each cascade grant.removed entry to the removed grant's own
  tenant, not the caller's. A platform admin deleting a role can remove
  tenant-scoped grants; those removals now land in the affected tenant's chain.
- R5-2 (P2): only return a purge checkpoint when rows were actually deleted. A
  no-op confirmed purge no longer mints a trust boundary that could legitimize a
  prefix it didn't authorize.
- R5-3 (P2): ensure the unique { chainKey, seq } index exists before appending
  (memoized createIndexes), so serialization doesn't depend on a background build
  — closes a silent chain-fork window under MONGO_AUTO_INDEX=false or at startup.

Tests: +per-grant-tenant cascade audit, +no-op-purge-no-checkpoint,
+index-built-before-append. ds auditLog 35, api roles 95 green.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-18 15:42:33 -04:00
Dustin Healy
fa20003952
🛂 refactor: Accept Targeted assign:configs for Config Scope-Lifecycle Endpoints (#13773)
* 🔓 fix: Accept Targeted assign:configs for Config Scope-Lifecycle Endpoints

Three admin-config endpoints currently require broad manage:configs: PUT /:principalType/:principalId for empty-overrides scope creation, DELETE /:principalType/:principalId for scope removal, and PATCH /:principalType/:principalId/active for the active toggle. The capability model already defines assign:configs:user|group|role for delegated administrators and validates that shape in isValidCapability, but no handler accepts it, so a delegate granted assign:configs:role via /api/admin/grants cannot manage scope lifecycle for the principal type they were explicitly delegated.

This aligns the server-side auth with the documented capability surface. Every destructive lateral path stays behind broad manage:configs: operations against the base config principal (__base__), non-empty PUT payloads that $set the full overrides field, and DELETE or toggle on a document whose existing overrides are non-empty (which would erase or neutralize sections the caller could not author). The new hasCapability dep on AdminConfigDeps is optional with a false default, so external consumers continue to get pre-PR behavior until they wire the resolver.

* 🛡️ fix: Block Assign-Only Scope-Lifecycle When Existing Doc Has Tombstones

The existing-overrides guard introduced in the prior commit only checked overrides, but configs also carry tombstones (suppressed inherited field paths) which are iterated during cascade resolution. An assign-only caller could delete, toggle, or empty-upsert a doc whose overrides is empty but whose tombstones is non-empty, which would erase or neutralize suppressions on fields they could not author. Extends the guard at all three call sites to treat a non-empty tombstones array as destructive state.

* 🚨 fix: Log TOCTOU Race When Assign-Only Lifecycle Op Hits Non-Empty Doc

The empty-state guard for assign-only callers performs a read-then-write across two DB roundtrips, so a concurrent broad-manage write can land between the guard and the destructive op. Adds post-write detection on the delete and toggle handlers: when the destructive op returns a doc whose state was non-empty at write time, emit logger.warn with the caller id, principal, and observed-state counts so ops can detect the race and restore from audit logs.

A fully atomic fix would require extending deleteConfig, toggleConfigActive, and upsertConfig in packages/data-schemas/src/methods/config.ts to support compare-and-swap filters, which is a wider design change than this PR's auth scope. Empty-payload upsert is not covered because $set replaces overrides, so the post-write doc no longer reflects pre-write state.

* 🔒 fix: Atomic Empty-State Filter for Assign-Only Scope-Lifecycle Writes

Replaces the read-then-check guard with an atomic Mongo filter on the destructive write itself. Adds an options.expectEmpty parameter to deleteConfig, toggleConfigActive, and upsertConfig in the shared data-schemas layer. When set, the filter requires both overrides and tombstones to be empty before the write matches. The TOCTOU race window is eliminated: a concurrent write cannot land between the empty-state check and the destructive op because they are now a single atomic operation.

For upsertConfig, the E11000 retry path returns null instead of falling back to a filterless update when expectEmpty is set, preserving the atomic property. Handlers fall back to findConfigByPrincipal only to disambiguate the null return between 404 (doc absent) and 403 (doc exists with non-empty state). The post-write logger.warn race detection added in the prior commit is removed as unreachable.
2026-06-18 15:40:58 -04:00
Danny Avila
68d142d0e9
🦜 refactor: Use path for Read/Write/Edit/Create File Tools (#13834)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix(agents): use `path` for read/write/edit/create file tools

Pairs with @librechat/agents renaming the read_file/write_file/edit_file tool
parameter from `file_path` to `path` (models — esp. Kimi K2 — emit `path` far
more reliably, and it matches grep/glob/list_directory which already use `path`).

- tools.ts: LibreChat's own code/skill file-tool schemas use `path`
  (the skill read_file tool inherits the SDK definition, which is already renamed)
- handlers.ts: read `args.path` for the model-facing tool arg + error messages
- the internal host `readSandboxFile`/`writeSandboxFile` contract is unchanged
- tests updated

Requires @librechat/agents with the param rename (danny-avila/agents#250). All
agents unit suites green (175).

* chore: update @librechat/agents to v3.2.41 and bump related dependencies in package-lock.json and package.json files

* fix(api): Refactor header merging in MCPConnection to use Object.assign for clarity

* test(e2e): mock emits `path` for create/edit file-authoring tools

The mock LLM still sent `file_path` for the create_file/edit_file calls, which the
renamed handlers no longer read -> the skill-file-authoring e2e failed with
'Expected skill to be persisted'. Switch the fixture to `path` to match the tools.
(The internal readSandboxFile/writeSandboxFile contract stays on `file_path`, so
api/server/services/Files/Code/process.js and its spec are unchanged.)
2026-06-18 14:44:51 -04:00
Danny Avila
a6b5343220
📦 chore: npm audit fix (#13828)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Publish `librechat-data-provider` to NPM / pack (push) Waiting to run
Publish `librechat-data-provider` to NPM / publish-npm (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 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
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🔧 chore: Update `@librechat/agents` to v3.2.38 and bump related dependencies in package-lock.json and package.json files

* 🔧 chore: Upgrade `multer` dependency to version 2.2.0 in package-lock.json and package.json

* 🔧 chore: Upgrade `nodemailer` dependency to version 9.0.1 in package-lock.json and package.json

* 🔧 chore: Upgrade `@aws-sdk/client-bedrock-agent-runtime` and `@aws-sdk/client-bedrock-runtime` to versions 3.1071.0, update related dependencies in package-lock.json and package.json

* 🔧 chore: Upgrade `form-data` to version 4.0.6 and `hono` to version 4.12.25, update related dependencies in package-lock.json and package.json

* 🔧 chore: npm audit fix

* 🔧 chore: Remove unused Babel dependencies from package-lock.json and package.json

* 🔧 chore: Add '@mistralai/mistralai' to esModules in Jest configuration files
2026-06-17 21:54:04 -04:00
Ravi Kumar L
27b0782201
📛 feat: Tag Langfuse Traces With Tenant ID (#13808)
* feat: tag Langfuse traces with tenant id

* fix: propagate tenant id to agent Langfuse config
2026-06-17 20:27:55 -04:00
Danny Avila
8628897c9c
📦 chore: Bump @librechat/agents to v3.2.37 (#13826) 2026-06-17 20:27:36 -04:00
Dan Lew
743f57f63e
🔖 feat: Add Pinned Conversations (#13492)
* feat: add `convo.pinned`

We want to be able to pin convos (so users can easily find them), thus we
added a new field to the DB schema: `pinned`.

We also had to add an API method for pinning a convo. It's got thorough tests.
It's structured just like how /api/convos/archive works, only for pinning.

* feat: add 'pinned' section to conversation list

If there are any pinned conversations, they will appear above the normal
"chats" list, with a pinned icon next to them.

* feat: added pin/unpin to convo options

ConvoOptions now has a pin/unpin button which lets you change the
pin status of any given conversation.

* fix: adjust ellipsizing gradient on ConvoLink

Because it went across the whole ConvoLink, it would cover up any
children (i.e. icons) that appear after the title. However, the point
of the gradient is just to gradually make the title disappear, not
the icons.

This change places the gradient on the title only, so it achieves
the same ellipsizing effect without interfering with the display of
the child icons.

* Fixed import sorting
2026-06-17 20:26:55 -04:00
Danny Avila
49f4b659f6
🔐 fix: Honor Admin-Panel MCP Allowlist Overrides Without Restart (#13814)
* 🔐 fix: Honor Admin-Panel MCP Allowlist Overrides Without Restart

MCPServersRegistry was built once at boot from getAppConfig({ baseOnly:
true }), freezing allowedDomains/allowedAddresses to YAML. Admin-panel
mcpSettings overrides were ignored by both inspection (addServer/
reinspectServer/updateServer/lazyInitConfigServer) and runtime connection
enforcement (assertResolvedRuntimeConfigAllowed), so a domain allowed only
via the panel failed inspection and never connected.

Make the registry's effective allowlists mutable and refresh them from the
merged admin-panel config: seed at boot, and re-apply on every config
mutation via invalidateConfigCaches -> clearMcpConfigCache. Both inspection
and connection paths read the same getters, so both honor overrides without
a restart. Fail-safe: current allowlists are preserved when the merged read
fails.

* 🛡️ fix: Scope MCP allowlist refresh to global config, fail-safe on DB error

Address Codex P1 review findings on the allowlist-refresh path:

- Tenant-scoped config mutations no longer push one tenant's merged
  mcpSettings into the process-wide registry singleton (read by all MCP
  connection paths), which would leak allowlists across tenants. Only
  global (non-tenant) mutations refresh the registry; tenant mutations
  still evict the config-server cache.
- The refresh read now uses strictOverrides:true so a transient DB error
  throws instead of silently returning YAML base config — preserving the
  last-known allowlists rather than overwriting them with fallback values.
  Adds the strictOverrides option to getAppConfig (default off, no behavior
  change for existing callers).

* ♻️ refactor: Resolve MCP allowlists per-request (tenant-scoped) instead of a global singleton

Supersedes the prior global-mutation approach. MCP allowlists live in
mcpSettings, which is tenant/principal-scoped admin config, so a process-wide
singleton value is the wrong model — it caused cross-tenant bleed and stale
reads.

Instead, inject a resolver (from the app layer, where the merged config lives)
that the registry calls per inspection and per connection. It reads the ALS
tenant context via getAppConfig and accepts the acting user so user/role-scoped
overrides resolve; config-source inspection (no user) resolves at tenant scope.
Falls back to the YAML base allowlists when no resolver is set or the lookup
fails, so a transient error fails to the operator baseline rather than
disabling the allowlist.

Removes the now-unnecessary setAllowlists / boot-seed / invalidateConfigCaches
refresh / getAppConfig.strictOverrides machinery.

* 🔒 fix: Scope config-source cache by allowlist; resolve OAuth allowlists per-request

Address Codex review of the per-request resolver:

- Config-source cache key now folds in the resolved allowlists, not just the
  raw-config hash. Inspection results became allowlist-dependent, so without
  this a tenant whose allowlist rejects a URL could poison the shared key with
  an inspectionFailed stub for a tenant that allows it (and vice versa). The
  tenant-scoped allowlist is resolved once per ensureConfigServers pass and
  threaded through the cache key + inspection.
- The two remaining request-time OAuth allowlist reads now use the merged
  config instead of the YAML base getters: the fallback OAuth-initiate path
  (routes/mcp.js) via resolveAllowlists, and OAuth revocation
  (UserController.maybeUninstallOAuthMCP) via the request's already-merged
  appConfig.mcpSettings. Without this, an OAuth endpoint allowed only by an
  admin-panel override was rejected while inspection/connection allowed it.

*  test: Update MCP OAuth registry/config mocks for per-request allowlists

CI fix for the Finding-12 change. The OAuth-initiate route now calls
registry.resolveAllowlists() and the revocation path reads the merged
appConfig.mcpSettings, so the affected specs' mocks were asserting the old
base-getter values:
- routes/__tests__/mcp.spec.js: add resolveAllowlists to the registry mock.
- UserController.mcpOAuth.spec.js: provide mcpSettings on the getAppConfig
  mock so revokeOAuthToken still receives the expected allowlists.

* 🧪 test: e2e proof that admin-panel MCP allowlist override takes effect

Adds a Playwright mock-harness spec for #13809. A URL-based MCP fixture
(e2e-http, streamable-http SDK server) boots inspectionFailed because its
origin is omitted from the YAML mcpSettings.allowedDomains; the spec adds that
origin via an admin config override (PUT /api/admin/config/user/:id) and
asserts the server reinitializes — exercising the real resolver path through
the backend + DB. Before the fix, reinspection used the frozen YAML allowlist
and the server stayed unreachable.

- e2e/setup/fake-mcp-http-server.js: streamable-HTTP MCP fixture (health GET /).
- e2e/playwright.config.mock.ts: boot the fixture as a second webServer.
- e2e/config/librechat.e2e.yaml: mcpSettings.allowedDomains (excludes 127.0.0.1)
  + the e2e-http server.
- e2e/specs/mock/mcp-allowlist-override.spec.ts: login → baseline reinit fails →
  apply override → reinit succeeds.
2026-06-17 20:14:53 -04:00
Danny Avila
c04bddd304
🪵 refactor: Bound Log Traversal And Remove Legacy api/config Logger (#13813)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🛡️ fix: Bound object-traverse against DAG fan-out and shared refs

Detect cycles via the ancestor chain (so shared, non-circular references in sibling branches / DAGs are traversed correctly) and add defensive maxNodes (100k) / maxDepth (100) caps. The removed global visited set was implicitly bounding work at O(distinct nodes); ancestor-chain-only detection is O(root-to-node paths), exponential on DAGs (a depth-24 diamond went from 26 to 50M visits / 1.6s of synchronous work). The caps bound it to ~9ms while leaving normal traversal untouched. Adds a spec covering shared refs, cycles, DAGs, and both bounds. The lone consumer, debugTraverse, inherits the defaults with no change.

* 🪵 refactor: Remove legacy api/config logger duplicate

The api/config winston logger was a stale parallel implementation of the canonical @librechat/data-schemas logger, with unbounded redaction (regex-only redactFormat, npm traverse-based debugTraverse). Its winston instance and the logger export from api/config/index.js had zero consumers — every ~/config importer uses the MCP/flow-manager exports. The only live tie was ToolService's use of redactMessage.

Re-export redactMessage from @librechat/data-schemas (behaviorally identical, a superset of the regex set), point ToolService at it, delete api/config/winston.js and api/config/parsers.js, drop the dead logger export, and remove the orphaned ~/config/parsers mock from the global test setup.

* 🧹 chore: Drop orphaned traverse dep and stale legacy logger tests

Deleting api/config/{winston,parsers}.js left the npm 'traverse' package unused in api/package.json (flagged by the detect-unused-packages CI check) and orphaned two tests that imported the deleted modules. Remove the traverse dependency (sync package-lock), and delete api/config/__tests__/{parsers,logToFile}.spec.js — the canonical logger's behavior is covered by packages/data-schemas/src/config/parsers.spec.ts.

* 🩹 fix: Make object-traverse caps bound work and survive update()

Address Codex review: (1) break the child loops as soon as the node budget is spent and iterate objects via for...in instead of materializing Object.entries/Object.keys, so maxNodes actually bounds work for wide arrays/objects; (2) detect ancestor cycles against an immutable original-node stack rather than context.node, which a callback's update() can reassign (the debug formatter rewrites array nodes in place). Adds tests for the wide-array bound and the update()-cycle case.

* 🎚️ fix: Tighten object-traverse defaults to a ~1ms log budget

Lower maxNodes 100000 -> 2500 and maxDepth 100 -> 5. Measured cost is ~140ns/node with the debug formatter callback, so 2500 nodes keeps a single log under ~1ms even on slower prod hardware; real log objects are ~25-30 nodes at depth 3-4, leaving ample headroom. maxNodes is the fan-out/cost lever; maxDepth bounds recursion and output readability (depth-5 covers typical logs, deeper renders compactly).
2026-06-17 12:31:32 -04:00
Danny Avila
6055ad0af2
🪃 fix: Restore Raw Spec Fallback for Enforced Presets (#13804)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (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 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
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: rebuild enforced specs from preset

* test: Add enforced model spec e2e coverage

* test: Align enforced spec regression scope
2026-06-16 21:10:22 -04:00
Danny Avila
fdc7e64bb7
🪙 feat: SDK-Aligned Context-Usage Projection (gauge for window-switch & snapshot-less branches) (#13801)
Some checks are pending
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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🪙 feat: Context-usage projection — data-provider + client wiring

Consumer side of the SDK-aligned context projection (agents
`projectAgentContextUsage`). Adds the `/api/endpoints/context-projection`
data-provider plumbing (endpoint, service, query key, `TContextProjectionRequest`)
and a `useContextProjectionQuery` gated to fire only when no fresh snapshot
covers the viewed branch.

Wires `useTokenUsage` precedence to: live snapshot → fresh persisted snapshot
(window matches the resolved one) → server projection → per-message estimate.
A model/window switch marks the baked snapshot stale (its `maxContextTokens`
no longer matches) and falls to the projection — closing the gauge's
window-switch (G1) and snapshot-less-branch (G2) gaps. Snapshot and projection
share the render-relevant fields, so they render uniformly.

Backend endpoint + agents version bump land in follow-up commits. Includes the
design spec (CONTEXT_PROJECTION_SPEC.md).

* 🪙 feat: Context-projection backend endpoint

POST /api/endpoints/context-projection → resolveContextProjection (packages/api):
reconstructs the viewed branch (parent-chain walk from messageId), resolves the
agent config (instructions/provider/model/maxContextTokens), reuses LibreChat's
stored per-message tokenCounts as the index map (no re-tokenizing), and calls
the agents SDK projectAgentContextUsage — no model call. Thin controller injects
db.getMessages/db.getAgent; route mirrors /token-config.

First cut targets message-windowing accuracy; tool-schema tokens are deferred to
a follow-up that reuses the full initializeAgent path.

* 🩹 fix: Codex review on context projection (G1 guard, IDOR, recount, summary)

- Guard `currentActive` against a stale window: a model/window switch on the
  current branch left the live snapshot outranking the projection (G1 didn't
  fire). Now defers to the projection unless streaming or the window matches.
- Scope branch lookups to the authenticated user (`getMessages` filter +
  injected `userId`) — was loading any conversation by id (IDOR).
- Recount messages with no stored `tokenCount` via the tokenizer instead of
  charging 0, so snapshot-less/imported histories don't under-report.
- Fall back (null) for already-summarized branches rather than projecting from
  the full raw parent chain (the next call would send summary + tail); the
  client's summary-baseline-aware estimate handles them until a follow-up
  replays the summary boundary.

* 🩹 fix: Codex round 2 — drop agent load, summary marker, edit-invalidation

- Stop loading agent/model-spec config server-side (closes the agent-access
  IDOR and the spec-prompt special-casing). Provider/model/window now come from
  the client-resolved request (`limits.endpoint`/model — the agent's real
  provider, not the `agents` endpoint, so the tokenizer is right). Agent/spec/
  promptPrefix instructions are uniformly deferred to the full-fidelity follow-up.
- Detect summarized branches via the live path's `metadata.summaryUsedTokens`
  marker (was the wrong `summaryTokenCount` field) and fall back to the
  summary-aware estimate.
- Invalidate the projection query on in-place message edits via a branch
  content `revision` in the cache key (the tail id is unchanged on edit).

Deferred (valid, not a regression): same-window endpoint/model switch keeps a
window-matched snapshot — needs endpoint/model persisted on the snapshot, which
lands with the fidelity follow-up. Smoke-tested: fits / prunes / summarized→null
/ no-window→null.

* 🛡️ fix: make context projection strictly additive (no-regression)

Revert the G1 window-match guard on the live/branch snapshot. When no explicit
maxContextTokens is set (the common default), the SDK's snapshot window is
reserve-derived (~0.9·(modelContext − maxOutputTokens)) while useTokenLimits
resolves the raw model context — so `snapshot.maxContextTokens === resolvedMax`
is false for the SAME model, and the guard would wrongly drop a valid
current-branch snapshot to projection/estimate post-stream (a regression in the
default case, per initialize.ts:1240-1243).

The projection now activates ONLY for snapshot-less branches (G2): the
precedence is live snapshot → persisted branch snapshot → projection → estimate,
where the first two are byte-for-byte the prior behavior and the projection just
slots ahead of the estimate. Window/model-switch (G1) detection needs the
snapshot to carry its model/window and defers to the fidelity follow-up.

* 🩹 fix: surface projections as estimates, not authoritative snapshots

A first-cut projection carries the SDK's windowing but omits instruction/tool
overhead, so rendering it as `isEstimate: false` showed a confident under-count
for snapshot-less branches. Mark projection-sourced views `isEstimate: true` +
`snapshotActive: false` (and drop the snapshot field) so they present as a
better estimate than sumBranch — improved used/window number, estimate framing,
no misleading granular breakdown with ~0 tools. Real snapshots stay
authoritative. (Codex round 3, projection.ts:139.)

* 🧹 chore: drop CONTEXT_PROJECTION_SPEC.md from the PR

* 🎨 style: fix import-sort order in projection.ts (CI sort-imports check)

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

* chore: npm audit fix

* 🎨 style: fix import-sort order in data-service.ts (CI sort-imports check)

* 🩹 fix: drop dead calibrationRatio in projectionParams (tsc never error)

Inside the ternary, branchSnapshot is narrowed to null (the gate is
), so  accessed a
property on  (frontend typecheck failure). It was also dead — there is
never a snapshot to seed from in this branch — so just remove it.

* Revert "chore: npm audit fix"

This reverts commit 4cdb862d0c.
2026-06-16 17:54:13 -04:00
Dustin Healy
054fa4bfa7
🥽 fix: Restrict MCP Server URL Disclosure to Admins, Owners, and Editors (#13784)
* 🥽 fix: Redact Non-User-Sourced MCP Server URLs by ACL Edit Permission

GET /api/mcp/servers and GET /api/mcp/servers/:serverName return MCP server configs to any caller with MCP-use permission. For user-sourced configs (DB-stored, UI-submitted), the URL is the caller's own and is intentionally disclosed. For non-user-sourced configs (YAML or config-tier, operator-defined), the URL and OAuth flow endpoints (authorization_url, token_url) are operator-sensitive: they can encode internal infrastructure hostnames and are not editable through the API.

This change redacts those fields on non-user-sourced configs unless the caller has edit authority on the resource, using the same ACL check (PermissionBits.EDIT) that the PATCH and DELETE routes already enforce via canAccessMCPServerResource. Callers with broad MANAGE_MCP_SERVERS capability bypass the per-resource check, matching the existing capability bypass in canAccessResource. customUserVars is intentionally not redacted: its values are UI hint metadata (title, description, sensitive), not user-supplied secrets; blanking it would give non-editor callers a Configure form with no field labels.

* 🥽 fix: Correct getResourcePermissionsMap import path + tighten redact comments

The MCP server redaction commit imported getResourcePermissionsMap from ~/server/controllers/PermissionsController, but that controller is a consumer of the helper, not its exporter. The canonical export lives in ~/server/services/PermissionService (which controllers/agents/v1.js already imports from). Fixes the runtime getResourcePermissionsMap is not a function failure on GET /api/mcp/servers and the four downstream route-spec failures whose config mocks lacked a source field and were therefore wrongly treated as non-user-sourced; mocks now reflect the real registry behavior (addServer/updateServer tag DB-stored configs with source: 'user'). Trims narrating JSDoc on the redact helpers and resorts the librechat-data-provider destructure by length.

* chore: import order

* 🥽 fix: Redact OAuth Revocation Endpoint Alongside Authorization And Token URLs

The OAuth-URL strip path only dropped authorization_url and token_url. The UserOAuthOptionsSchema in packages/data-provider/src/mcp.ts (line 146) accepts revocation_endpoint as another operator-configurable URL, and the OAuth handler uses it to revoke tokens; it can hold the same internal IdP hostnames the existing strip is trying to hide. Adds revocation_endpoint to the destructure so a non-user-sourced YAML/config MCP server config no longer leaks the revocation URL to non-editor callers. The existing strip url and oauth flow URLs spec is extended with a revocation_endpoint value to lock in the new field.

* 🥽 fix: Gate Shared DB Server URL Disclosure On ACL Edit Permission

source-driven URL disclosure was incorrect for shared DB-backed MCP servers. ServerConfigsDB.mapDBServerToParsedConfig (packages/api/src/mcp/registry/db/ServerConfigsDB.ts:465) sets source: 'user' on every DB-stored config it returns, regardless of who is accessing it. A user with only VIEW share on a DB server, or with agent-mediated access, was therefore treated by the redaction layer as if they owned the URL, and GET /api/mcp/servers disclosed the owner's URL and OAuth flow URLs to viewers who could not edit the resource.

The redaction is now driven purely by ACL edit authority: computeCanEditByServer routes every dbId-bearing config through PermissionBits.EDIT regardless of source; redactServerSecrets strips on !canEdit regardless of source. POST and PATCH controllers explicitly pass canEdit: true since both endpoints establish edit authority (POST creates the resource, PATCH is gated on the EDIT middleware). Legacy/ephemeral configs without a dbId still fall back to the source heuristic.

* 📝 docs: correct redactServerSecrets URL-disclosure comment

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-16 11:20:52 -04:00
Danny Avila
d0f659fa75
🗜️ fix: Support Windows ZIP MIME Uploads (#13794) 2026-06-16 11:19:06 -04:00
Danny Avila
d18d62e7c1
🪙 refactor: Reconcile Context Gauge to Actual Provider Tokens (#13780)
* 🪙 fix: Reconcile Context Gauge to Actual Provider Tokens

The context gauge could read several× too high (e.g. 213K when the real prompt
was 56K) and stay there across reloads. Root cause: the SDK's calibrationRatio is
`cumulativeProviderReported / cumulativeRawSent`, but a provider's server-side
web search injects large fetched content into the prompt that the SDK never sent
or counted — pinning the ratio at its cap (5) and multiplying every later message
estimate, including post-summary ones. The gauge rendered (and persisted) that
inflated estimate, never the provider's actual token count.

Fix: reconcile the snapshot to the call's ACTUAL prompt tokens (input + cache),
which already arrive in on_token_usage. Only messageTokens is calibration-scaled
(instructions/summary are raw tiktoken), so keep those and set messageTokens to
the remainder, recomputing free space. Shared `promptTokensFromUsage` +
`reconcileContextUsage` in data-provider; applied server-side in
buildPersistedContextUsage (reload-stable) and client-side in useUsageHandler on
each primary usage (corrects at turn-end, no follow-up needed). Also drop the
summary double-count from the Breakdown Messages row.

Deferred (separate agents PR): the SDK over-calibration also fires summarization
prematurely; fixing it needs decoupling real-content estimation from server-side
injection headroom without weakening pruning-overflow safety.

* 🪙 fix: Harden Token Reconciliation for Provider-less + Resume Paths

Codex review on the reconciliation:
- promptTokensFromUsage: when the provider is absent (custom/OpenAI-compatible
  payloads), fall back to the same magnitude heuristic normalizeUsageUnits uses
  (cache ≤ input ⇒ already included) so cached events aren't re-inflated.
- Resume: backfillUsage restores a primary call's usage without replaying a live
  on_token_usage (Redis mode), so the live reconcile never ran and a reconnected
  session stayed on the inflated estimate. New reconcileBackfill reconciles the
  restored snapshot from the final primary call after contextHandler installs it.

* 🪙 fix: Reconcile Resume Snapshot Server-Side, Not via Backfill

Codex: the client reconcileBackfill scanned the resumed run's collectedUsage and
applied the final primary to the latest snapshot — but on a mid-call resume that
usage belongs to an EARLIER call, corrupting the restored gauge.

Move the resume reconciliation server-side: GenerationJobManager.persistTokenUsage
reconciles the stored contextUsage to a primary usage's actual prompt tokens as it
arrives. That usage is the post-invoke truth for the call the latest stored
snapshot precedes (no snapshot is captured between a call's pre-invoke dispatch
and its usage), so it's correct by construction and run-matched. A mid-call resume
(no usage yet) keeps the raw snapshot instead of mis-applying an earlier call's
tokens; it reconciles once the call completes. Removed client reconcileBackfill;
the live-path reconcile (non-resume) stays.

* 🪙 fix: Guard Reconciliation Against Replays and Snapshot Races

Two Codex concurrency findings on the reconciliation:
- Client: reconcile only on a NEWLY folded primary usage. A replayed duplicate
  (folded=false on resume) can be an earlier tool-loop call sharing the run id,
  which would overwrite the latest snapshot with an earlier, smaller prompt. Moved
  the reconcile after the folded guard.
- Server: serialize the context-usage write through the same per-stream queue as
  the token-usage write. persistTokenUsage reconciles the stored snapshot
  (read-modify-write); an unserialized trackContextUsage could store a newer
  snapshot between the read and write — or a stale reconciled write could land
  after a newer snapshot — clobbering the newer run's gauge when calls interleave.
  FIFO keeps each call's snapshot ahead of its own usage and behind the next.

* chore: import order in GenerationJobManager.ts
2026-06-16 11:05:44 -04:00
Danny Avila
055585f9f1
🪢 fix: Tie MCP Cleanup To Resumable Runs (#13769)
Some checks failed
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
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* fix: Clean up request-scoped MCP connections

* test: Format MCP request context spec

* refactor: Move MCP request context to API package
2026-06-15 15:26:03 -04:00
Danny Avila
0537930144
🗂️ fix: Scope Token Config Cache (#13770)
* fix token config tenant cache scope

* fix token config scoped cache backfill

* chore sort token config imports
2026-06-15 15:25:19 -04:00
Danny Avila
b917e0418b
v0.8.7-rc1 (#13592)
* chore: Bump LibreChat to v0.8.7-rc1

* docs: Sync Chinese README
2026-06-15 13:10:30 -04:00
Ravi Kumar L
fbc990f684
📈 fix: Isolate RUM Telemetry Proxy Auth from App Auth (#13765)
* fix(rum): isolate telemetry proxy auth

* feat(rum): track proxy error metrics

* refactor(rum): simplify proxy auth strategy flow

* test(rum): clarify proxy success metric assertion

* test(metrics): use typed supertest import

* test(metrics): add local supertest types

* test(metrics): keep supertest types local

* test(metrics): use official supertest types

* fix(rum): log proxy auth strategy errors

* fix(rum): classify proxy auth errors in metrics

* style(rum): sort telemetry metric imports

* ci: mention import sort check command

* ci: show targeted import sort example
2026-06-15 12:49:44 -04:00
Danny Avila
44c253d48a
🪙 fix: Correct Context Usage Gauge After Summarization (#13744)
* 🪙 fix: Persist Context Snapshot + Summary Marker After Summarization

The post-summarization context is correctly compacted by the SDK, but the
breakdown wasn't reliably reaching the client, leaving the gauge on the
whole-history estimate (stuck at 100% forever once a conversation compacts).

Two server changes in buildResponseMetadata:
- Snapshot guard: persist the breakdown when a PRIMARY usage event follows the
  latest snapshot (tracked via contextUsageSink.latestUsageIndex, recorded in
  the on_context_usage handler) instead of a brittle snapshot-vs-primary count.
  A summarization detour adds an extra snapshot whose only following usage is
  tagged 'summarization', which the count guard could miscount and drop.
- Summary marker: whenever a turn compacts (summaryTokens > 0), persist a
  lightweight metadata.summaryUsedTokens (the pre-invoke compacted context size)
  UNCONDITIONALLY — so even when the full snapshot can't be saved (interrupted
  final call) or never reaches the client, the per-message estimate has a signal
  to cap the discarded history.

Tests: client.contextMetadata.spec (guard + marker, incl. marker-survives-drop)
and a real-pipeline summarization integration test.

* 🪙 fix: Cap the Context Estimate at the Summary Marker

When the gauge falls back to the per-message estimate (no usable snapshot on the
branch), sumBranch summed the ENTIRE branch history — after a summarization that
discarded most of it, this over-counts and pins the gauge at 100% in perpetuity.

sumBranch now stops at the deepest summarized response (metadata.summaryUsedTokens)
and records it as summaryBaseline; the walk counts only post-summary messages,
and useTokenUsage adds the baseline. So the estimate reflects the compacted
context (summary + recent turns), not the discarded history. USD/default
behavior unchanged when no marker is present.

Test: sumBranch caps a huge pre-summary history at the compacted baseline.

* 🪙 fix: Address Codex Review on the Summarization Marker

- Branch cost/usage is no longer truncated at the summary marker — sumBranch
  caps only the CONTEXT-window count there and keeps accumulating provider
  usage/cost to the root (cumulative spend isn't discarded by compaction).
- findBranchSnapshotAnchor stops at a summarized response with no snapshot of its
  own, so it can't recover a stale PRE-summary snapshot and show discarded
  history; the summary-baseline estimate is used instead.
- Abort path: buildAbortedResponseMetadata now persists the summaryUsedTokens
  marker (pre-invoke, no completedOutputTokens ambiguity, so safe on abort) so a
  STOPPED summarized turn isn't re-summed on reload.
- Marker baseline fallback now includes summaryTokens (a separate breakdown
  field) so it doesn't under-report the compacted size. DRY'd into a shared
  computeSummaryUsedTokens used by the completion and abort paths.
- Estimate popover surfaces the summary baseline as a row so the displayed rows
  reconcile with the header total.

Tests: sumBranch cost-not-truncated + anchor-stops-at-marker (client);
computeSummaryUsedTokens fallback + abort marker (packages/api).

* 🪙 fix: Attribute Persisted Context Usage to the Snapshot Run

Match the post-snapshot primary usage to the latest snapshot's runId before
persisting metadata.contextUsage. Parallel/direct runs interleave snapshots and
usage (A snapshot → B snapshot → A usage → B no-usage); the prior index-only
guard persisted B's snapshot with A's output. finalCallOutputTokens now filters
completedOutputTokens to the snapshot's run. Untagged events (older lib/resume)
match any run for back-compat.

* 🪙 fix: Harden Summary Marker Against Tool-Loops, Stale Anchors, and Emit Races

Codex round on the summarization marker:

- Avoid double-counting earlier tool-loop outputs in the summary marker: those
  outputs sit in BOTH the latest snapshot's pre-invoke baseline AND the response
  message's tokenCount the client estimate adds on top. computeSummaryUsedTokens
  now subtracts the run's prior primary outputs (priorRunOutputTokens) — the live
  path bounds them by the snapshot's usage index, the abort path by all primaries
  (an interrupted final call emits none). Single-call turns subtract 0.
- Stop treating pre-summary anchors as active: sumBranch no longer sets
  containsAnchor once the context is capped at a summary marker, so a stale
  pre-summary snapshot can't override the summary-baseline estimate.
- Capture latestUsageIndex BEFORE awaiting emitEvent: a yield (resumable SSE /
  Redis) during parallel runs could let this call's own usage advance the index
  past the event that proves the snapshot completed, dropping a valid breakdown.

* 🪙 fix: Subtract Summarization Output from the Summary Marker

recordCollectedUsage folds the summarization call's completion into the response
message's tokenCount, while the generated summary is also in the snapshot baseline
as summaryTokens. The client estimate (summaryBaseline + responseTokenCount) thus
counted the summary twice — inflating the gauge after compaction even on a
single-call turn whenever the full snapshot is unavailable. priorRunOutputTokens
now also counts summarization-tagged output (still excluding subagent/sequential,
which recordCollectedUsage keeps out of the reported total), so the marker
subtracts it. Updated unit + guard tests.

* 🪙 fix: Refine Marker Subtraction for Summarization RunId and Abort Boundary

Two Codex follow-ups on the marker-subtraction logic:

- Subtract summarization output regardless of runId: the summarize detour is its
  own model-end call that may carry a distinct runId, but its output still lands
  in this response's tokenCount AND the snapshot baseline (summaryTokens). It is
  now counted unconditionally (still within the response's own usageEmitSink),
  while primaries keep the parallel-run runId filter.
- Don't subtract primaries on the abort path: the job stores no snapshot/usage
  boundary, so a primary that completed AFTER the latest snapshot is NOT in the
  baseline; subtracting it would cancel real output and under-report. priorRun-
  OutputTokens gains an includePrimary flag (false for abort) — abort subtracts
  only the always-pre-snapshot summarization output.

* 🪙 fix: Run-Scope Summary Subtraction and Stop Subtracting on Abort

Two Codex follow-ups, resolved by reverting the round-4 detour:

- Run-scope the summarization subtraction: the summarize detour inherits the
  graph run id (traceConfig spreads config.metadata.run_id), so its usage shares
  the answer snapshot's runId — it is NOT a distinct run. priorRunOutputTokens now
  filters summarization by runId like primaries, so a parallel sibling run's
  summary (different runId, in the sibling's baseline) is no longer subtracted from
  this branch's marker. Drops the includePrimary flag added last round.
- Stop subtracting on the abort path: abort tokenCount is countTokens(text)
  (abortMiddleware) or absent (agents route) — it does not fold in summarization or
  earlier-call output the way recordCollectedUsage does, so the marker must keep
  the full baseline. buildAbortedResponseMetadata now subtracts nothing.
2026-06-14 18:23:30 -04:00
Danny Avila
2350ebb24a
📨 feat: Custom Headers on Built-in Provider Endpoints (#13742)
* 📨 feat: Custom Headers on Built-in Provider Endpoints

Add a `headers` config option to the built-in `openAI`, `anthropic`, and
`google` endpoints (incl. Anthropic/Google Vertex), mirroring the custom
endpoint header mechanism. Values support the same placeholder resolution
(env vars, `{{LIBRECHAT_USER_*}}`, `{{LIBRECHAT_BODY_CONVERSATIONID}}`) and
are resolved at request time so dynamic values like conversationId resolve
against the live request — without losing provider-native request shaping.

Closes #13082. Covers #13713: forwarding conversationId to a reverse proxy
is now `X-Conversation-Id: '{{LIBRECHAT_BODY_CONVERSATIONID}}'` — an unknown
header is ignored by the native Anthropic API, so no 400 and no metadata
gating needed.

- Schema: `headers` on `baseEndpointSchema` (openAI/google/anthropic/all).
- New `mergeHeaders`/`resolveConfigHeaders` utils centralize the per-provider
  header locations (`configuration.defaultHeaders`, Anthropic
  `clientOptions.defaultHeaders`, Google `customHeaders`); provider-managed
  headers (auth, `anthropic-beta`) always win on collision.
- Each initializer threads configured headers (endpoint over `all`) into the
  right place; request-time resolution runs across all locations in the main
  and title flows.

* 🩹 fix: Cast endpoints.all to TEndpoint for headers DeepPartial widening

Adding `headers` (a Record) to `baseEndpointSchema` makes `DeepPartial<TCustomConfig>`
widen its value type to `string | undefined`, which is not assignable to the
concrete `TEndpoint['headers']: Record<string, string>` at the `loadedEndpoints.all`
assignment. Cast at the assignment site, mirroring the existing
`anthropicConfig as TAnthropicEndpoint` cast in the same function.

* 🛡️ fix: Harden built-in endpoint custom headers (Codex review)

Address Codex P2 findings on the custom-headers feature:

- Anthropic title requests: `omitTitleOptions` strips the `clientOptions`
  carrier, which dropped its `defaultHeaders`. Preserve just the header carrier
  so gateway/reverse-proxy metadata still reaches title generation.
- mergeHeaders: match header names case-insensitively so an override (e.g. a
  provider-managed `Authorization`/`anthropic-beta`) replaces/uniones a
  case-variant from the base instead of emitting two names a client may collapse.
- OpenAI: withhold admin-configured headers when the user supplies the base URL
  (`user_provided`), since values may carry `${SECRET}`/token placeholders that
  must not reach a user-controlled endpoint — mirrors the custom-endpoint guard.
- Azure: honor global `endpoints.all` headers (same OpenAI carrier) while keeping
  Azure-managed `api-key`/version headers authoritative.

Adds tests for each.

* 🔐 fix: Resolve-once + provider-managed header safety (Codex review round 2)

Address Codex P2 findings:

- Azure: keep global `endpoints.all` headers unresolved at init and let
  request-time `resolveConfigHeaders` resolve them once, avoiding a
  second-order env expansion of already-substituted user values.
- Google: `resolveConfigHeaders` no longer template-resolves the
  provider-managed `Authorization` header (built from a possibly user-provided
  key), so a user key like `${ENV}` can't leak server environment values.
- Model fetches: thread configured headers (endpoint over `all`) + user object
  through `getOpenAIModels`/`getAnthropicModels` → `fetchModels`, so a
  gateway-fronted built-in provider receives the header on `/models` too. Fixed
  `fetchModels` to merge custom headers for Anthropic instead of overwriting
  them (managed `x-api-key`/version still win).

Adds/updates tests for each.

* 🧯 fix: Header provenance, memory/title coverage, idempotency (Codex round 3)

Address Codex P2 findings, including two regressions from the prior round:

- Google auth (findings 6 & 8): move native Google header resolution to init
  (`initializeGoogle`), resolving admin templates BEFORE the key-derived auth
  header is built. resolveConfigHeaders no longer touches Google `customHeaders`,
  so admin `Authorization` templates resolve again (fixes the round-2 regression)
  while the SDK auth header (possibly a user-provided key) is never env-expanded.
- Memory runs: memory extraction now calls `resolveConfigHeaders`, so native
  Anthropic (and OpenAI) headers resolve for memory requests too.
- Vertex titles: restore the ORIGINAL `clientOptions` object reference (not a
  copy) when preserving headers across `omitTitleOptions`, so the Vertex
  `createClient` closure and the resolved headers stay on the same object.
- Reuse: `resolveConfigHeaders` is now idempotent (resolve-once per header map),
  preventing a second pass from env-expanding values already substituted with
  user/body data when an agent object flows through buildAgentInput twice.

Adds/updates tests for each.
2026-06-14 17:02:04 -04:00
Danny Avila
4ee68d5240
💸 feat: Per-Agent Endpoint Token Config in Multi-Endpoint Billing (#13738)
* 💸 feat: Per-Agent Endpoint Token Config in Multi-Endpoint Billing

Price each collected/emitted usage item with the producing agent's resolved
endpoint token config, instead of the primary agent's for the whole graph.

Previously AgentClient.recordCollectedUsage and the subagent usage emitter used
a single this.options.endpointTokenConfig (the primary's) for every usage item.
A connected agent or subagent on a different custom endpoint that shares a model
id with an entry in the primary's tokenConfig was therefore mis-priced (a model
absent from it already fell back to the built-in rate map — no regression).

- Tag each usage with its producing agent: ModelEndHandler stamps
  usage.agentId = agentContext.agentId; createSubagentUsageSink stamps the
  child's subagentAgentId (UsageMetadata gains an optional agentId).
- buildAgentToolContext retains endpointTokenConfig so initialize.js can build
  an agentId -> endpointTokenConfig map from agentToolContexts (the one map that
  holds every agent, including pure subagents pruned from agentConfigs).
- AgentClient.resolveAgentEndpointTokenConfig(usage) looks up that map by
  agentId, falling back to the primary config; used by both the billing path
  (new optional resolveEndpointTokenConfig on recordCollectedUsage) and the
  subagent cost emitter.
- recordCollectedUsage's resolver is optional and falls back to the batch
  endpointTokenConfig, so the shared responses.js/openai.js call sites are
  unchanged.
- Tests: two-endpoint graph with a colliding model id prices per-agent; resolver
  nullish falls back to batch; subagent sink tags the child agent id.

* fix: Align emit-path cost with per-agent billing; honor known-agent built-in pricing

Addresses Codex review on the per-agent endpoint token config:
- Emit path (callbacks.js) now prices each on_token_usage event with the
  producing agent's config (resolved via usageCost.resolveEndpointTokenConfig),
  so streamed/persisted metadata.usage.cost matches the per-agent balance
  transaction. The agentId tag is resolved server-side and stripped from the
  emitted/persisted payload.
- Resolver (resolveAgentTokenConfig) now treats a known agent's config as
  authoritative, including undefined → built-in pricing, so a known non-custom
  agent in a custom-primary graph is no longer charged the primary's rates.
  Only untagged/unknown usage falls back to the primary config.
- endpointTokenConfigByAgentId records every known agent (value may be
  undefined) so the resolver distinguishes known-no-rates from unknown.
2026-06-14 12:00:32 -04:00
Danny Avila
b03b2a0a29
💾 feat: Persist Context Breakdown & Branch/Total Usage Cost (#13734)
* 💾 feat: Persist Context Breakdown & Branch/Total Usage Cost

Persist the granular context breakdown and per-response usage/cost on the
response message metadata, and re-derive branch + total usage/cost from a
per-message index so the popover survives reloads and is branch-aware live.

- Add aggregateEmittedUsage + buildPersistedContextUsage helpers in
  packages/api; capture the latest visible snapshot and every emitted
  on_token_usage payload via contextUsageSink/usageEmitSink.
- Attach metadata.contextUsage (Part A) and metadata.usage (Part B) on the
  agents response message in sendCompletion.
- Carry per-message usage on the token index; add sumTotalUsage/setEntryUsage
  and branch-scoped usage on sumBranch.
- Repurpose the session accumulator into a single in-flight pending holder;
  flush it into the index at finalize; hydrate breakdowns on load.
- Render branch cost with a conditional all-branches total in the breakdown.

* 🧹 chore: Remove orphaned com_ui_session_cost i18n key

* 🩹 fix: Address Codex review — normalize usage server-side, fix reload deltas

- Persist per-event-normalized display units in metadata.usage (TResponseUsage)
  so reloaded mixed-provider turns match the live session; client reads them
  directly instead of re-normalizing with a single stamped provider (P2).
- Persist completedOutputTokens (final call output) on metadata.contextUsage so
  a reloaded multi-call turn adds the post-snapshot delta, not the full
  tokenCount the snapshot already counts (P2).
- buildIndex preserves a prior entry's immutable usage when a rebuilt cache
  message lacks metadata.usage, so a mid-session rebuild (regenerate) keeps a
  sibling branch's flushed cost (fixes the e2e regenerate failure).
- Track costKnown so turns saved with contextCost off don't render $0.00 when
  cost display is later enabled (P3).
- Use an epsilon for the all-branches cost comparison to avoid a spurious total
  row from float summation order (P3).
- Update unit/integration/e2e tests for the new shapes; regenerate e2e asserts
  the all-branches total after reload (deterministic via persisted metadata).

* 🩹 fix: Address Codex round 2 — pending leak, cost coverage, reload delta

- Clear the in-flight pending usage on terminal abort/error (resetLive), so a
  stopped generation's tokens no longer merge into the next response (P2).
- costKnown now means COMPLETE coverage (ANDed): a branch mixing cost-bearing
  and cost-less turns is flagged incomplete and the cost row is hidden rather
  than rendering an under-reported total (P2).
- Drop the tokenCount fallback for completedOutputTokens on reload: only the
  persisted post-snapshot delta is used, so a multi-call turn whose provider
  emitted no usage_metadata no longer double-counts earlier output (P2).
- Update tokens.spec for AND coverage semantics + incomplete-cost case.

* 🩹 fix: Address Codex round 3 — no-usage snapshots, total coverage, provider-less cache

- Skip persisting metadata.contextUsage when the response emitted no primary
  usage event: without a known post-snapshot output the granular gauge would
  undercount the reply on reload, so fall back to the coarse per-message
  estimate instead (P2).
- Gate the all-branches cost row on totalUsage.costKnown so an incomplete total
  (a sibling saved without cost) never renders an under-reported figure (P2).
- aggregateEmittedUsage/finalCallOutputTokens now normalize per-event with the
  client's magnitude fallback (normalizeEventUnits) instead of billing
  splitUsage, so provider-less cached events match live on reload (P2).
- Add backend test for the provider-less cached case.

* 🩹 fix: Address Codex round 4 — abort attribution, complete cost coverage

- aggregateEmittedUsage persists cost only when EVERY call was priced; a partial
  pricing failure now omits cost so the client treats coverage as unknown rather
  than reading an under-reported sum as authoritative (P2).
- finalizeUsage flushes pending into the response entry only when events were
  folded this session (eventCount > 0), so a late/second resumable subscriber
  carrying persisted metadata.usage keeps it instead of being overwritten with
  an empty pending record (P2).
- On user stop, attribute the in-flight pending usage to the partial response
  (new attributePending handler) instead of discarding it in resetLive — the
  stopped reply's billed tokens are kept and still can't leak into the next
  response; resetLive's discard remains for the error path (P2).

* 🐛 fix: Persist branch cost across branch switches via sticky usage history

Branch cost vanished on switching to a sibling branch (until a new turn) — the
cost analog of the granularity bug. buildIndex rebuilds the token index from the
messages cache; a sibling generated this session whose cache message lacks
metadata.usage (and is transiently dropped from the cache during regenerate)
lost its live-flushed usage, so sumBranch found none and the cost row hid.

Fix: a sticky per-response usage map (conversationId → messageId → usage),
written by setEntryUsage and never rebuilt from the cache — the usage counterpart
of snapshotsByAnchorFamily for the breakdown. buildIndex/upsertEntries restore an
entry's usage from it when the message carries none; cleared on convo switch and
migrated with the index. Add unit coverage for the drop-then-readd regression and
an e2e assertion that branch cost survives a branch switch.

* 🐛 fix: Re-index on branch switch so branch cost survives the switch

The sticky usage history alone didn't fix the reported branch-switch cost drop:
on a branch switch no cache `updated` event fires, so the index subscriber never
re-ran, and the post-regenerate rebuild was skipped while `isSubmitting` was
still true — leaving the index stale and missing the now-viewed branch's
response entirely (sticky can only restore entries present in a rebuild).

Re-index from the messages cache on every tail change (created/finalize AND
branch switch), not just while submitting. The cache holds the full message set
at switch time, so the viewed branch's response is re-added and its usage
restored from metadata.usage or the sticky history → sumBranch finds it and the
branch cost renders. Verified locally: the branch-switch e2e now passes (the
cost section shows both the branch row and the all-branches total). Also fixed
that e2e assertion to target a single cost value (strict-mode safe).

* 🩹 fix: Handle stopped-stream usage — reset pending + persist abort metadata

Codex round (stop/abort edges):
- Resumable explicit-stop (intentional SSE close) reset UI state but never
  cleared pendingUsageFamily, so usage folded before the stop leaked into the
  next response in the conversation. Discard pending on intentional close
  (resetLive); a resume re-folds via backfillUsage, so nothing is lost.
- The abort save path (abortMiddleware) persisted the stopped response without
  metadata.usage/contextUsage, so its cost + breakdown vanished on reload.
  Rebuild both from the job's persisted tokenUsage (emitted payloads incl. cost)
  and contextUsage snapshot — parity with the normal sendCompletion path;
  breakdown gated on a primary usage event like buildResponseMetadata.

Deferred (per scope decision): mid-stream branch-switch transiently shows the
streaming branch's pending on the viewed sibling (cosmetic, until finalize).

* 🩹 fix: Persist abort metadata on the real agents route + tighten snapshot gate

Codex round (corrects last round's wrong-path fixes):
- Stopped AGENTS responses are saved by routes/agents/index.js (/chat/abort),
  not abortMiddleware — so last round's metadata fix never ran for them. Moved
  the rollup/snapshot builder into packages/api as buildAbortedResponseMetadata
  (shared, unit-tested) and applied it in BOTH abort save paths, so a stopped
  agent reply keeps its cost + breakdown on reload.
- Persist the breakdown only when the FINAL visible call emitted usage: track a
  per-response snapshot count and require primaryUsageCount >= snapshotCount.
  Previously any earlier primary usage event passed the gate, so a multi-call
  turn whose final call emitted no usage_metadata used an earlier call's output
  as completedOutputTokens (already counted by the latest snapshot) → reload
  over-reported. Now it falls back to the coarse estimate.

Resumable stop pending-reset (prior round, 3cde6fe035) already flows through
clearAllSubmissions → SSE close → the intentional-close handler's resetLive.
Deferred per scope: mid-stream branch-switch pending attribution (tracked).

* 🩹 fix: Abort breakdown over-count + resume re-fold after pending discard

Codex round (on the re-applied abort/snapshot work):
- buildAbortedResponseMetadata now persists ONLY the usage/cost rollup, not the
  context breakdown. The abort path can't tell whether the final call emitted
  usage (the job stores only the latest snapshot, not a count), so persisting
  the breakdown risked reusing an earlier call's output as completedOutputTokens
  (already in the snapshot) → reload over-count. Stopped/incomplete responses
  now fall back to the coarse gauge estimate, which is safe and apt.
- resetLive now also forgets the conversation's folded usage-event identities
  (clearUsageFolded). Discarding pending on a terminal/intentional close left
  the folded keys set, so a later resume's backfillUsage saw the persisted
  events as duplicates and never rebuilt pending — leaving the response's usage
  missing until a full reload. Clearing them lets the resume re-fold.
2026-06-14 10:48:07 -04:00
Danny Avila
98704f28c1
🌐 fix: Centralize Outbound Proxy Handling (#13726)
* fix: centralize outbound proxy handling

* chore: sort proxy imports

* test: update proxy helper mocks

* fix: honor proxy bypasses consistently

* fix: support http axios proxy targets
2026-06-14 10:47:49 -04:00