feat: add ephemeral retention mode for forced temporary chats

Extends the `retentionMode` interface option with a new `ephemeral` value
that forces every conversation to be temporary and applies expiration
deadlines to all data. Unlike `all` (which keeps chats visible until they
expire), `ephemeral` marks all chats temporary so they are hidden from
history and search, with the per-chat toggle locked on.

- Add RetentionMode.EPHEMERAL plus isAllDataRetention and
  isForcedTemporaryRetention helpers
- Force isTemporary and expiry on conversations, messages, imports, and files
- Force the TEMPORARY_CHAT permission on so the locked indicator is shown
- Lock the temporary-chat toggle in the UI and force new/existing chats temporary
- Document the new mode in librechat.example.yaml
This commit is contained in:
Marco Beretta 2026-06-17 01:11:26 +02:00
parent bc6b032421
commit 219ce08872
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
17 changed files with 238 additions and 51 deletions

View file

@ -7,8 +7,9 @@ const {
const {
EModelEndpoint,
Constants,
RetentionMode,
openAISettings,
isAllDataRetention,
isForcedTemporaryRetention,
} = require('librechat-data-provider');
const { bulkIncrementTagCounts, bulkSaveConvos, bulkSaveMessages } = require('~/models');
const { FALLBACK_MODEL_BY_ENDPOINT } = require('./defaults');
@ -45,19 +46,20 @@ class ImportBatchBuilder {
return this.retentionFields;
}
if (this.interfaceConfig?.retentionMode !== RetentionMode.ALL) {
if (!isAllDataRetention(this.interfaceConfig?.retentionMode)) {
this.retentionFields = {};
return this.retentionFields;
}
const isTemporary = isForcedTemporaryRetention(this.interfaceConfig?.retentionMode);
try {
this.retentionFields = {
isTemporary: false,
isTemporary,
expiredAt: createTempChatExpirationDate(this.interfaceConfig),
};
} catch (error) {
logger.error('[ImportBatchBuilder] Error creating import expiration date:', error);
this.retentionFields = { isTemporary: false, expiredAt: createFallbackRetentionDate() };
this.retentionFields = { isTemporary, expiredAt: createFallbackRetentionDate() };
}
return this.retentionFields;
}

View file

@ -1145,6 +1145,23 @@ describe('importLibreChatConvo', () => {
expect(result.conversation.expiredAt).toBeInstanceOf(Date);
expect(result.conversation.expiredAt).toBe(message.expiredAt);
});
it('marks imported conversations and messages temporary under ephemeral retention', () => {
const requestUserId = 'user-123';
const builder = new ImportBatchBuilder(requestUserId, {
retentionMode: RetentionMode.EPHEMERAL,
temporaryChatRetention: 24,
});
builder.startConversation(EModelEndpoint.openAI);
const message = builder.addUserMessage('Ephemeral import');
const result = builder.finishConversation('Imported ephemeral chat');
expect(message.isTemporary).toBe(true);
expect(message.expiredAt).toBeInstanceOf(Date);
expect(result.conversation.isTemporary).toBe(true);
expect(result.conversation.expiredAt).toBeInstanceOf(Date);
expect(result.conversation.expiredAt).toBe(message.expiredAt);
});
});
});