* feat: support data retention for normal chats
Add retentionMode config variable supporting "all" and "temporary" values.
When "all" is set, data retention applies to all chats, not just temporary ones.
Adds isTemporary field to conversations for proper filtering.
Adapted to new TS method files in packages/data-schemas since upstream
moved models out of api/models/.
Based on danny-avila/LibreChat#10532
Co-Authored-By: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com>
(cherry picked from commit 30109e90b0)
* feat: extend data retention to files, tool calls, and shared links
Add expiredAt field and TTL indexes to file, toolCall, and share schemas.
Set expiredAt on tool calls, shared links, and file uploads when
retentionMode is "all" or chat is temporary.
(cherry picked from commit 48973752d3)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: lint/test
(cherry picked from commit 310c514e6a)
* fix: address code review feedback for data retention PR
Critical:
- Fix BookmarkMenu crash: restore optional chaining on conversation
- Fix migration hazard: backward-compatible sidebar filter that also
checks expiredAt for documents without isTemporary field
Major:
- Add logging to getRetentionExpiry error path, align with tools.js
- Add tests for retentionMode: ALL in saveConvo and saveMessage
- Fix share route: apply expiredAt for temporary chats too by
querying the conversation's isTemporary flag server-side
- Add assertions for getRetentionExpiry mocks in process tests
Minor:
- Fix ChatRoute isTemporaryChat to be strictly boolean via Boolean()
- Fix stale test description (expired -> temporary)
- Comment out retentionMode default in example yaml
- Simplify verbose if/else to isTemporary === true
- Add compound index on { user: 1, isTemporary: 1 }
- Remove narrating comment from process.spec.js
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6bad535f90)
* chore: fix typescript
(cherry picked from commit 826527a46b)
* fix: lint
(cherry picked from commit 77817e80ea)
* fix: use mockSanitizeArtifactPath in retention test
The 'getRetentionExpiry is called with the request object' test
referenced an undefined `mockSanitizeFilename` identifier, breaking
both lint (no-undef) and the test suite. Use the existing
`mockSanitizeArtifactPath` mock that the surrounding tests already
use, since `processCodeOutput` calls `sanitizeArtifactPath` (not
`sanitizeFilename`) before invoking `getRetentionExpiry`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 52ea2da66d)
* fix: forward isTemporary from client for retention on file uploads and tool calls
Server-side `getRetentionExpiry` (file uploads) and the tool-call
controller both read `req.body.isTemporary`, but the file upload
multipart form and the tool-call payload did not include that field.
In `retentionMode: temporary` (default), files uploaded and tool
calls created from temporary chats were therefore retained
indefinitely.
Forward the Recoil `isTemporary` flag in both client paths so the
existing server checks can fire correctly. `ToolParams` gains an
optional `isTemporary` field.
Addresses Codex P1 review feedback on PR #29.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7e937df05a)
* test: stub store.isTemporary in useFileHandling test mocks
Previous commit added `useRecoilValue(store.isTemporary)` to the
hook. The test file mocks `~/store` with only `ephemeralAgentByConvoId`
and does not stub `useRecoilValue`, so all 7 cases threw
"Invalid argument to useRecoilValue: expected an atom or selector but
got undefined". Add a stub default export with `isTemporary` and a
`useRecoilValue` mock returning `false`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit eb1609537d)
* fix: harden data retention semantics
* fix: provide sweep request context for expired files
* fix: preserve temporary flags in all-retention updates
* fix: honor assistant versions in retention sweeps
* fix: retain non-temporary flags in all mode
* fix: hide expired retained records
* fix: propagate retained conversation expiry
* fix: refresh meili retention cutoff
* fix: prevent overlapping file sweeps
* fix: show legacy retained conversations
* fix: index legacy retained records
* fix: harden retention cleanup edge cases
* fix: count failed file storage sweeps
* fix: preserve legacy temporary retention
* fix: assign retention sweep worker deterministically
* fix: hide expired shared links on reads
* fix: prevent retention refresh after parent expiry
* fix: break code output retention import cycle
* fix: harden retention review findings
* fix: ignore expired share duplicates
* fix: reject expired retained share creation
* fix: harden retention review edge cases
* fix: address retention audit findings
* fix: enforce expired conversation shares in all retention
* fix: scope temporary upload flag to chat files
* fix: address retention review findings
* fix: address codex retention review findings
* fix: tighten missing storage detection
* test: remove unused file process spec bindings
---------
Co-authored-by: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com>
Co-authored-by: Aron Gates <aron@muonspace.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 📦 chore: npm audit fix 2026-05-18
- Added @js-sdsl/ordered-map version 4.4.2
- Updated @librechat/agents to version 3.1.87
- Upgraded @opentelemetry/sdk-node to version 0.218.0
- Added new dependencies for gRPC and OpenTelemetry exporters
* 🔧 chore: Update @librechat/agents to version 3.1.87 in package-lock.json and package.json files
* 🔧 chore: Upgrade @opentelemetry/sdk-node to version 0.218.0 in package.json and package-lock.json
Four jest mocks for `winston` in the test suite return the wrong shape:
api/test/__mocks__/logger.js (returns inner fn directly)
packages/api/src/agents/__tests__/memory.test.ts (`format` is a plain object)
packages/api/src/agents/__tests__/run-summarization.test.ts (same)
packages/api/src/agents/__tests__/initialize.test.ts (same)
Real `winston.format(fn)` returns a Format constructor whose instances
expose a `.transform(info, opts)` method that winston's pipeline calls
with the log info object. The current mocks collapse this:
- `(fn) => fn` returns the inner transform fn directly. When module-load
code in `@librechat/data-schemas/dist/config/parsers.cjs:52` does
`const redactFormat = winston.format((info) => ...)`, `redactFormat`
becomes the inner fn. The next line in `winston.cjs` calls
`parsers.redactFormat()` which invokes the inner fn with no `info`,
throwing `TypeError: Cannot read properties of undefined (reading 'level')`.
- `format: { combine, colorize, simple }` makes `winston.format` not
callable at all — `winston.format((info) => ...)` throws
`TypeError: winston.format is not a function`.
These currently pass in CI on GitHub Actions Ubuntu / Node 20.19, but
fail reproducibly on Node 24.x and on some Linux distros (verified on
WSL Ubuntu with Node 24.9.0). The CI passes appears to be environmental
luck around jest's mock-hoisting interaction with the workspace symlink
chain — the mocks are genuinely wrong against the data-schemas contract.
The fix: return a thunk that yields `{ transform: fn }` — matches real
winston's shape just enough that module-load completes; the inner fn is
only ever invoked by winston's pipeline (never at load time). Also adds
the full `winston.format.*` method surface (printf, timestamp, errors,
splat, json) plus `addColors` and the `DailyRotateFile`/`File` transports
that data-schemas's dist code references at module-load.
Verification (Node 24.9.0):
npm run build:data-provider && npm run build:data-schemas && npm run build:api
cd packages/api && npx jest src/agents/__tests__/{memory,run-summarization,initialize}.test.ts
→ 3 suites, 106 tests, all pass
No production code or behavior changes — test-only patch.
Co-authored-by: Jorge Costa <8352477+JorgeCosta87@users.noreply.github.com>
* 📦 chore: Bump `@librechat/agents` to v3.1.86 in package-lock.json and package.json files
* 📦 chore: Update dependencies in package-lock.json to latest versions, including @protobufjs/codegen, @protobufjs/inquire, @protobufjs/utf8, and protobufjs
* 📦 chore: Add `librechat-data-provider` dependency in package.json and package-lock.json, and update build dependencies in turbo.json
* 📦 chore: Update @librechat/agents to version 3.1.85 in package-lock.json and package.json files
* 📦 chore: Update mermaid to version 11.15.0 in package.json and package-lock.json
* refactor: Streamline batch upload error handling in `uploadCodeEnvFile`
* refactor: Enhance session info error logging in `getSessionInfo`
* refactor: Update error logging to use `logAxiosError` in various agent handlers and skill file processing functions
* refactor: Consolidate missing resource checks in `createToolExecuteHandler` for better clarity
* 🛟 fix: Allow empty modelSpecs.list to unstick admin-panel saves
The unconditional `.min(1)` on `specsConfigSchema.list` rejected an empty
list even when `enforce: false`, leaving admin panels (which save fields
path-granularly) with no atomic way to clear the list once it had been
populated. Once an admin reached `list: [entry]` and deleted the only
entry, every subsequent save failed schema validation and the section
became stuck.
Relax the schema to `.default([])`. The `.min(1)` was added in #5218 as
part of bundled cleanup, not as a deliberate rule. Every consumer of
`modelSpecs.list` already handles the empty/undefined case (`?.list`,
`?? []`, length-checked), and `processModelSpecs` short-circuits to
`undefined` when the list is empty so the runtime treats it as "no
specs configured." No call site is load-bearing on length >= 1.
Tighten the `buildEndpointOption.js` enforce guard from
`?.list && ?.enforce` to `?.list?.length && ?.enforce`. Empty arrays
are truthy in JS, so the existing guard would have entered the enforce
branch on `list: []` and returned "No model spec selected" or "Invalid
model spec" had `processModelSpecs` ever been bypassed.
Add a runtime warn in `processModelSpecs` when `enforce: true` is
configured alongside an empty list, so operators see the resulting
"enforcement disabled" state in logs rather than silently getting a
permissive runtime.
Add coverage for the empty-list parse path in `config-schemas.spec.ts`
and for the empty-list-with-enforce branch in `buildEndpointOption.spec.js`.
* chore: update import order in config-schemas.spec.ts
When a tool round-trip is interrupted between the tool result and the
model's text reply (user aborted, network drop, pod restart, ...) and
LibreChat persists the partial assistant message, the next conversation
turn reconstructs an `AIMessage` from `formatAgentMessages` that has
`tool_calls` populated but no `additional_kwargs.signatures`. Vertex
Gemini 3 rejects the resumed request with 400 because the most recent
historical functionCall has no `thought_signature`.
## Storage shape
Capture as `Record<tool_call_id, signature>` rather than a flat array.
This addresses the codex P1 review:
> When an assistant turn contains multiple sequential tool-call batches,
> this restoration path writes all persisted thoughtSignatures onto only
> the last tool-bearing AIMessage. Vertex/Gemini validates signatures
> for each step in the current tool-calling turn, so earlier
> functionCall steps reconstructed without their signature can still
> fail with 400.
A single agent run can fire multiple `chat_model_end` events when the
loop cycles the LLM with intervening tool results — each cycle owns a
distinct `tool_call_id`. Per-id storage maps each signature back onto
the right reconstructed `AIMessage`, not just the last one.
## Mapping
`additional_kwargs.signatures` is a flat array indexed by *response part*
(text + functionCall interleaved). `tool_calls` is just the function
calls in their original order. Non-empty signatures correspond 1:1 with
tool_calls in order — see `partsToSignatures` in
`@langchain/google-common`. Single-pass walk maps `signatures[i]` (when
non-empty) onto the i-th `tool_call.id`.
## Pipeline
| Stage | File | Change |
|---|---|---|
| Capture | callbacks.js | `ModelEndHandler` accepts `Record<string,string>` map; walks signatures + tool_calls in tandem to record per-id. Gated on the map being provided — non-Vertex flows are no-op (and also no-op even when provided, since they don't emit signatures). |
| Plumbing | initialize.js | Allocate `collectedThoughtSignatures = {}`, share with handler + client. Always allocated; the JSDoc explicitly documents that it stays empty for non-Vertex providers. |
| Surface | client.js | `sendCompletion` returns `metadata.thoughtSignatures` when the map has entries; falls through unchanged when empty. |
| Persist | (existing BaseClient.handleRespCompletion) | Writes `metadata` from `sendCompletion` onto `responseMessage.metadata`. Mongoose `Mixed` — no migration. |
| Restore | formatMessages.js | Track every tool-bearing AIMessage produced from a TMessage. For each, build a position-aligned `additional_kwargs.signatures` array (empty placeholders for tool_calls without a stored sig). Agents' `fixThoughtSignatures` dispatches non-empty entries to functionCall parts in order. |
## Live verification
- **Single-step:** real Vertex `gemini-3.1-flash-lite-preview` resume-after-tool case. With fix ✅ / without ❌ 400.
- **Multi-step (codex case):** real two-step agent loop (list /tmp → echo done). Each step's signature attaches to its own reconstructed AIMessage. With fix ✅ / without ❌ 400.
- **Cross-provider:** Anthropic Claude haiku-4.5 + OpenAI gpt-5-mini accept the persisted/restored shape unchanged.
## Tests
`modelEndHandler.spec.js` (new) — 6 tests:
- maps non-empty signatures onto tool_call_ids in order
- accumulates per-id across multiple `model_end` events (multi-step)
- no-op when `collectedThoughtSignatures` is null
- no-op when `signatures` field missing (non-Vertex)
- no-op when `tool_calls` missing
- preserves existing `collectedUsage` array contract
`formatAgentMessages.spec.js` — 6 new tests:
- restores onto the AIMessage that owns the tool_call
- per-step attachment for multi-step turns (codex review case)
- preserves tool_call ordering when signatures are partial
- no-op when metadata.thoughtSignatures absent
- no-op when assistant has no tool_calls
- no-op when stored ids don't match any current tool_call
37 passing across 3 suites; 15 existing formatAgentMessages tests unchanged.
## Compatibility
- Backward-compatible — restore gated on `metadata.thoughtSignatures` being a populated object; capture gated on the map being provided.
- No schema migration — uses `Message.metadata: Mixed` already in place.
- Cross-provider safe — non-Vertex providers tolerate the field (verified live against Anthropic + OpenAI converters).
- Pairs with [agents#159](https://github.com/danny-avila/agents/pull/159) for full coverage on histories that mix plain-text and toolcall AIMessages.