fix: enforce forced retention on per-conversation tag writes

Bookmark-tag writes update Conversation rows directly without saveConvo, so under
ephemeral retention adding a tag to a chat (createConversationTag) or changing a
chat's tag list (updateTagsForConversation) left an older permanent conversation
with isTemporary/expiredAt unset, keeping it visible and non-expiring.

Make applyForcedRetention's messageId optional so it can run the conversation
cascade alone, load app config on the per-conversation tag routes (POST /api/tags
when adding to a conversation, PUT /api/tags/convo/:conversationId), and enforce
retention after the write.
This commit is contained in:
Marco Beretta 2026-06-23 08:59:42 +02:00
parent b7187cbe59
commit 955f41c4ed
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
3 changed files with 70 additions and 13 deletions

View file

@ -8,9 +8,10 @@ const {
createConversationTag,
deleteConversationTag,
getConversationTags,
applyForcedRetention,
getRoleByName,
} = require('~/models');
const { requireJwtAuth } = require('~/server/middleware');
const { requireJwtAuth, configMiddleware } = require('~/server/middleware');
const router = express.Router();
@ -23,6 +24,17 @@ const checkBookmarkAccess = generateCheckAccess({
router.use(requireJwtAuth);
router.use(checkBookmarkAccess);
/**
* Enforces forced (ephemeral) retention after a bookmark-tag write converts an older
* permanent conversation; a no-op outside forced retention.
*/
const enforceForcedRetention = (req, conversationId, context) =>
applyForcedRetention(
{ userId: req?.user?.id, interfaceConfig: req?.config?.interfaceConfig },
{ conversationId },
{ context },
);
/**
* GET /
* Retrieves all conversation tags for the authenticated user.
@ -49,9 +61,12 @@ router.get('/', async (req, res) => {
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
router.post('/', async (req, res) => {
router.post('/', configMiddleware, async (req, res) => {
try {
const tag = await createConversationTag(req.user.id, req.body);
if (req.body?.addToConversation && req.body?.conversationId) {
await enforceForcedRetention(req, req.body.conversationId, 'POST /api/tags');
}
res.status(200).json(tag);
} catch (error) {
logger.error('Error creating conversation tag:', error);
@ -107,13 +122,18 @@ router.delete('/:tag', async (req, res) => {
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
router.put('/convo/:conversationId', async (req, res) => {
router.put('/convo/:conversationId', configMiddleware, async (req, res) => {
try {
const conversationTags = await updateTagsForConversation(
req.user.id,
req.params.conversationId,
req.body.tags,
);
await enforceForcedRetention(
req,
req.params.conversationId,
'PUT /api/tags/convo/:conversationId',
);
res.status(200).json(conversationTags);
} catch (error) {
logger.error('Error updating conversation tags', error);