⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.237.004"
VERSION = "0.237.005"


SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
Expand Down
29 changes: 15 additions & 14 deletions application/single_app/functions_retention_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
This module handles automated deletion of aged conversations and documents
based on configurable retention policies for personal, group, and public workspaces.

Version: 0.237.004
Version: 0.237.005
Implemented in: 0.234.067
Updated in: 0.236.012 - Fixed race condition handling for NotFound errors during deletion
Updated in: 0.237.004 - Fixed critical bug where conversations with null/undefined last_activity_at were deleted regardless of age
Updated in: 0.237.005 - Fixed field name: use last_updated (actual field) instead of last_activity_at (non-existent)
"""

from config import *
Expand Down Expand Up @@ -487,7 +488,7 @@ def process_public_retention():

def delete_aged_conversations(retention_days, workspace_type='personal', user_id=None, group_id=None, public_workspace_id=None):
"""
Delete conversations that exceed the retention period based on last_activity_at.
Delete conversations that exceed the retention period based on last_updated.

Args:
retention_days (int): Number of days to retain conversations
Expand Down Expand Up @@ -521,16 +522,16 @@ def delete_aged_conversations(retention_days, workspace_type='personal', user_id
cutoff_iso = cutoff_date.isoformat()

# Query for aged conversations
# ONLY delete conversations that have a valid last_activity_at that is older than the cutoff
# Conversations with null/undefined last_activity_at should be SKIPPED (not deleted)
# This prevents accidentally deleting new conversations that haven't had activity tracked yet
# ONLY delete conversations that have a valid last_updated that is older than the cutoff
# Conversations with null/undefined last_updated should be SKIPPED (not deleted)
# This prevents accidentally deleting new conversations that haven't had their timestamp set
query = f"""
SELECT c.id, c.title, c.last_activity_at, c.{partition_field}
SELECT c.id, c.title, c.last_updated, c.{partition_field}
FROM c
WHERE c.{partition_field} = @partition_value
AND IS_DEFINED(c.last_activity_at)
AND NOT IS_NULL(c.last_activity_at)
AND c.last_activity_at < @cutoff_date
AND IS_DEFINED(c.last_updated)
AND NOT IS_NULL(c.last_updated)
AND c.last_updated < @cutoff_date
"""

parameters = [
Expand Down Expand Up @@ -571,7 +572,7 @@ def delete_aged_conversations(retention_days, workspace_type='personal', user_id
deleted_details.append({
'id': conversation_id,
'title': conversation_title,
'last_activity_at': conv.get('last_activity_at'),
'last_updated': conv.get('last_updated'),
'already_deleted': True
})
continue
Expand Down Expand Up @@ -653,7 +654,7 @@ def delete_aged_conversations(retention_days, workspace_type='personal', user_id
deleted_details.append({
'id': conversation_id,
'title': conversation_title,
'last_activity_at': conv.get('last_activity_at')
'last_updated': conv.get('last_updated')
})

debug_print(f"Deleted conversation {conversation_id} ({conversation_title}) due to retention policy")
Expand Down Expand Up @@ -844,7 +845,7 @@ def send_retention_notification(workspace_id, deletion_summary, workspace_type):
notification_type='system_announcement',
title='Retention Policy Cleanup',
message=full_message,
link_url='/chat',
link_url='/chats',
metadata={
'conversations_deleted': conversations_deleted,
'documents_deleted': documents_deleted,
Expand All @@ -857,7 +858,7 @@ def send_retention_notification(workspace_id, deletion_summary, workspace_type):
notification_type='system_announcement',
title='Retention Policy Cleanup',
message=full_message,
link_url='/chat',
link_url='/chats',
metadata={
'conversations_deleted': conversations_deleted,
'documents_deleted': documents_deleted,
Expand All @@ -870,7 +871,7 @@ def send_retention_notification(workspace_id, deletion_summary, workspace_type):
notification_type='system_announcement',
title='Retention Policy Cleanup',
message=full_message,
link_url='/chat',
link_url='/chats',
metadata={
'conversations_deleted': conversations_deleted,
'documents_deleted': documents_deleted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,12 @@ function createSidebarConversationItem(convo) {
const titleWrapper = document.createElement('div');
titleWrapper.classList.add('sidebar-conversation-header', 'd-flex', 'align-items-center', 'flex-grow-1', 'overflow-hidden', 'gap-2');

// Ensure the title can truncate correctly within the new wrapper
// Insert the wrapper before the dropdown first
headerRow.insertBefore(titleWrapper, dropdownElement);

// Now move the title element into the wrapper
originalTitleElement.classList.add('flex-grow-1', 'text-truncate');
originalTitleElement.style.minWidth = '0';

titleWrapper.appendChild(originalTitleElement);

const isGroupConversation = (convo.chat_type && convo.chat_type.startsWith('group')) || groupName;
Expand All @@ -160,8 +162,6 @@ function createSidebarConversationItem(convo) {
badge.title = groupName ? `Group conversation: ${groupName}` : 'Group conversation';
titleWrapper.appendChild(badge);
}

headerRow.insertBefore(titleWrapper, dropdownElement);
}

// Add double-click editing to title
Expand Down
201 changes: 201 additions & 0 deletions docs/explanation/fixes/v0.237.005/RETENTION_POLICY_FIELD_NAME_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Retention Policy Field Name Fix

## Version: 0.237.005

## Problem Statement

The retention policy was not deleting any conversations despite being configured with valid retention periods. After the v0.237.004 fix that required conversations to have a valid timestamp field, the policy found zero conversations to delete because it was querying for a field (`last_activity_at`) that doesn't exist on any conversation document.

### Symptoms
- Retention policy execution shows "0 conversations deleted" even with old conversations present
- Manual execution completes successfully but no conversations are affected
- Conversations older than the retention period remain in the database

### Debug Output Example
```
[DEBUG] [INFO]: Querying aged conversations: workspace_type=personal, retention_days=1
[DEBUG] [INFO]: Found 0 aged conversations for personal workspace
```

## Root Cause Analysis

### The Field Mismatch

The retention policy SQL query was looking for `last_activity_at`:

```sql
SELECT c.id, c.title, c.last_activity_at, c.user_id
FROM c
WHERE c.user_id = @partition_value
AND IS_DEFINED(c.last_activity_at)
AND NOT IS_NULL(c.last_activity_at)
AND c.last_activity_at < @cutoff_date
```

However, **all conversation schemas use `last_updated`**, not `last_activity_at`:

| Schema Version | Era | Field Used |
|----------------|-----|------------|
| Schema 1 (messages embedded) | Legacy | `last_updated` |
| Schema 2 (messages separate) | Middle | `last_updated` |
| Schema 3 (messages with threading) | Current | `last_updated` |

### Example Conversation Documents

**Schema 1 (Legacy - messages embedded):**
```json
{
"id": "2ff663f2-f260-4a21-a388-dc8f60caa353",
"user_id": "441f7b4e-2f43-4a83-abf1-40697309b24d",
"messages": [...],
"last_updated": "2025-03-04T21:09:23.945024",
"title": "how do i know if i'm being sca..."
}
```

**Schema 2 (Middle - messages in separate container):**
```json
{
"id": "4d45051a-693d-4893-8960-9c4c2dc6b8be",
"user_id": "07e61033-ea1a-4472-a1e7-6b9ac874984a",
"last_updated": "2025-08-01T18:58:10.137683",
"title": "what did paul win"
}
```

**Schema 3 (Current - messages with threading):**
```json
{
"id": "bba2f03e-aa9a-4cee-a8fb-273e0c89c834",
"user_id": "07e61033-ea1a-4472-a1e7-6b9ac874984a",
"last_updated": "2026-01-27T21:00:43.910594",
"title": "tell me about https://microsof...",
"context": [...],
"tags": [...],
"strict": false,
"is_pinned": false,
"is_hidden": false
}
```

### Why `last_activity_at` Never Existed

The field `last_activity_at` was likely a planned feature that was never implemented. All conversation creation and update code paths use `last_updated`:

```python
# From route_backend_conversations.py
conversation_item = {
'id': conversation_id,
'user_id': user_id,
'last_updated': datetime.utcnow().isoformat(), # ← Only 'last_updated' is set
'title': 'New Conversation',
...
}
```

## Solution Implementation

### File Modified: `functions_retention_policy.py`

Changed all references from `last_activity_at` to `last_updated`:

#### 1. Updated SQL Query

```python
# Before (incorrect field)
query = f"""
SELECT c.id, c.title, c.last_activity_at, c.{partition_field}
FROM c
WHERE c.{partition_field} = @partition_value
AND IS_DEFINED(c.last_activity_at)
AND NOT IS_NULL(c.last_activity_at)
AND c.last_activity_at < @cutoff_date
"""

# After (correct field)
query = f"""
SELECT c.id, c.title, c.last_updated, c.{partition_field}
FROM c
WHERE c.{partition_field} = @partition_value
AND IS_DEFINED(c.last_updated)
AND NOT IS_NULL(c.last_updated)
AND c.last_updated < @cutoff_date
"""
```

#### 2. Updated Docstring

```python
# Before
"""Delete conversations that exceed the retention period based on last_activity_at."""

# After
"""Delete conversations that exceed the retention period based on last_updated."""
```

#### 3. Updated Result Dictionaries

```python
# Before
deleted_details.append({
'id': conversation_id,
'title': conversation_title,
'last_activity_at': conv.get('last_activity_at')
})

# After
deleted_details.append({
'id': conversation_id,
'title': conversation_title,
'last_updated': conv.get('last_updated')
})
```

## Files Modified

| File | Changes |
|------|---------|
| `config.py` | Version updated to `0.237.005` |
| `functions_retention_policy.py` | Changed `last_activity_at` → `last_updated` in query, docstring, and result dictionaries |

## Testing & Validation

After the fix, retention policy execution should correctly identify and delete old conversations:

```
[DEBUG] [INFO]: Querying aged conversations: workspace_type=personal, retention_days=1
[DEBUG] [INFO]: Found 3 aged conversations for personal workspace
[DEBUG] [INFO]: Deleted conversation abc123 (Test Conversation) due to retention policy
```

### Test Scenarios

| Scenario | Before Fix | After Fix |
|----------|------------|-----------|
| Conversation with valid `last_updated` older than retention | ❌ Not found (wrong field) | ✅ Deleted |
| Conversation with valid `last_updated` newer than retention | ✅ Kept | ✅ Kept |
| Conversation with null `last_updated` | ✅ Skipped | ✅ Skipped |

## Schema Compatibility

This fix ensures compatibility with all three conversation schemas that exist in production:

| Schema | Messages Location | `last_updated` Field | Retention Works? |
|--------|-------------------|---------------------|------------------|
| 1 (Legacy) | Embedded in conversation | ✅ Present | ✅ Yes |
| 2 (Middle) | Separate container | ✅ Present | ✅ Yes |
| 3 (Current) | Separate container + threading | ✅ Present | ✅ Yes |

## Version History

| Version | Issue | Fix |
|---------|-------|-----|
| 0.237.003 | Initial retention policy implementation | N/A |
| 0.237.004 | Conversations with null `last_activity_at` deleted | Required valid timestamp field |
| 0.237.005 | Query used non-existent field | Changed to `last_updated` field |

## Related Documentation

- [v0.237.004 Null Last Activity Fix](../v0.237.004/RETENTION_POLICY_NULL_LAST_ACTIVITY_FIX.md)
- [v0.236.012 NotFound Error Fix](../v0.236.012/RETENTION_POLICY_NOTFOUND_FIX.md)
- [Retention Policy Feature Documentation](../../features/RETENTION_POLICY.md)
15 changes: 15 additions & 0 deletions docs/explanation/release_notes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
<!-- BEGIN release_notes.md BLOCK -->
# Feature Release

### **(v0.237.005)**

#### Bug Fixes

* **Retention Policy Field Name Fix**
* Fixed retention policy to use the correct field name `last_updated` instead of the non-existent `last_activity_at` field.
* **Root Cause**: The retention policy query was looking for `last_activity_at` field, but all conversation schemas (legacy and current) use `last_updated` to track the conversation's last modification time.
* **Impact**: After the v0.237.004 fix, NO conversations were being deleted because the query required a field that doesn't exist on any conversation document.
* **Schema Support**: Now correctly supports all 3 conversation schemas:
* Schema 1 (legacy): Messages embedded in conversation document with `last_updated`
* Schema 2 (middle): Messages in separate container with `last_updated`
* Schema 3 (current): Messages with threading metadata with `last_updated`
* **Solution**: Changed SQL query to use `last_updated` field which exists on all conversation documents.
* (Ref: retention policy execution, conversation deletion, `delete_aged_conversations()`, `last_updated` field)

### **(v0.237.004)**

#### Bug Fixes
Expand Down