Skip to content

Commit 07850a8

Browse files
committed
feat: implement background cleanup for orphaned attachments
Provides an automated way to manage storage by removing attachment directories associated with deleted or missing sessions. - Initialize a background thread on startup to prune orphaned attachments. - Update `AttachmentManager.cleanup_orphaned_attachments` with CPU yielding and a startup delay to minimize impact on performance. - Implement a 60-second age threshold safety check to prevent race conditions during new session creation. - Add logic to track removed session IDs in `session_manager.py` to facilitate cleanup.
1 parent e90bcaf commit 07850a8

File tree

4 files changed

+85
-2
lines changed

4 files changed

+85
-2
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## [4.3.2] - 2026-02-15
4+
5+
### Improvements
6+
7+
- **Attachments**: Implemented a background cleanup process to automatically remove orphaned attachment directories from deleted or missing sessions.
8+
9+
### Fixes
10+
11+
- **Console**: Bugfixes and adjustments to make console work reliably.
12+
313
## [4.3.1] - 2026-02-14
414

515
### Fixes

main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from src.version import __version__
3030
from src.key_manager import KeyManager
3131
from src.session_manager import load_sessions, list_sessions
32+
from src.attachment_manager import AttachmentManager
3233
from src.terminal import terminal_session_manager, print_commands_box
3334
from src.gui.core import HAVE_GUI, show_settings_window_blocking
3435
from src import web_server
@@ -178,6 +179,11 @@ def initialize():
178179
# ─── Sessions ─────────────────────────────────────────────────────────
179180
load_sessions()
180181
sessions = list_sessions()
182+
183+
# Cleanup orphaned attachments from deleted sessions
184+
# (Runs in background to avoid slowing down startup)
185+
threading.Thread(target=AttachmentManager.cleanup_orphaned_attachments, daemon=True).start()
186+
181187
if HAVE_RICH:
182188
console.print(f"[bold]📂 Sessions[/bold] {len(sessions)} loaded")
183189
console.print()

src/attachment_manager.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,28 +508,67 @@ def cleanup_orphaned_attachments(cls) -> int:
508508
Compares attachment directories against existing sessions and
509509
removes any orphaned directories.
510510
511+
Built with CPU usage optimization:
512+
- Checks file age before deleting (skips very new files to avoid race conditions)
513+
- Yields CPU during heavy deletion operations
514+
- Runs once and exits (designed for background thread usage)
515+
511516
Returns:
512517
Number of orphaned directories removed
513518
"""
514519
attachments_path = Path(ATTACHMENTS_DIR)
515520
if not attachments_path.exists():
516521
return 0
522+
523+
# Startup delay to ensure session manager is fully loaded and stable
524+
# and to degrade priority effectively
525+
time.sleep(2.0)
517526

518527
removed = 0
519528

520529
try:
521530
# Get existing session IDs
522531
from .session_manager import CHAT_SESSIONS
532+
533+
# Create a localized set of valid IDs to minimize lock contention
534+
# We assume session deletions during this millisecond window are acceptable handling edge cases
523535
existing_ids = set(str(sid) for sid in CHAT_SESSIONS.keys())
524536

525-
# Check each attachment directory
537+
# Iterate through directories
526538
for item in attachments_path.iterdir():
527-
if item.is_dir() and item.name not in existing_ids:
539+
# Yield CPU between directory checks to keep background usage low
540+
time.sleep(0.05)
541+
542+
if item.is_dir():
543+
# Check if folder name is numeric (session ID)
544+
# We only manage numeric session IDs
545+
if not item.name.isdigit():
546+
continue
547+
548+
# Skip if session exists
549+
if item.name in existing_ids:
550+
continue
551+
552+
# SAFETY CHECK: Only delete folders older than 60 seconds
553+
# This prevents deleting attachments for a session that is currently being created
554+
# (where the folder might exist before it's registered in CHAT_SESSIONS)
555+
try:
556+
stat = item.stat()
557+
if time.time() - stat.st_mtime < 60:
558+
continue
559+
except OSError:
560+
pass
561+
562+
# Force delete orphaned folder
528563
try:
529564
with _FILE_LOCK:
530565
shutil.rmtree(item)
531566
logging.info(f"[AttachmentManager] Removed orphaned: {item.name}")
532567
removed += 1
568+
569+
# Longer sleep after actual deletion work as it's I/O heavy
570+
time.sleep(0.2)
571+
533572
except Exception as e:
534573
logging.warning(f"[AttachmentManager] Failed to remove {item.name}: {e}")
535574

src/session_manager.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,41 @@ def load_sessions():
214214

215215
def add_session(session, max_sessions=50):
216216
"""Add a session and manage max limit"""
217+
removed_ids = []
217218
with SESSION_LOCK:
218219
while len(CHAT_SESSIONS) >= max_sessions:
219220
oldest_id = next(iter(CHAT_SESSIONS))
220221
del CHAT_SESSIONS[oldest_id]
222+
removed_ids.append(oldest_id)
221223
CHAT_SESSIONS[session.session_id] = session
224+
222225
threading.Thread(target=save_sessions, daemon=True).start()
223226

227+
# Cleanup attachments for removed sessions
228+
# (Only runs if items were actually removed)
229+
if removed_ids:
230+
def cleanup():
231+
import time
232+
from .attachment_manager import delete_session_attachments
233+
234+
# Initial delay to let the main operation finish
235+
time.sleep(2.0)
236+
237+
for sid in removed_ids:
238+
try:
239+
# Clean up attachments - handle string/int IDs
240+
numeric_id = int(sid) if isinstance(sid, str) and sid.isdigit() else sid
241+
if isinstance(numeric_id, int):
242+
delete_session_attachments(numeric_id)
243+
# Yield CPU after each deletion
244+
time.sleep(0.1)
245+
except Exception as e:
246+
print(f"[Warning] Failed to cleanup session {sid}: {e}")
247+
248+
# Start cleanup in background
249+
t = threading.Thread(target=cleanup, daemon=True)
250+
t.start()
251+
224252

225253
def get_session(session_id):
226254
"""Get a session by ID (handles both string and int IDs)"""

0 commit comments

Comments
 (0)