feat(01-03): add /archive command to compress and remove sessions

Archives session directory with tar+pigz to sessions_archive/,
terminates any running subprocess first, clears active session if needed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikkel Georgsen 2026-02-04 18:01:14 +00:00
parent 3cc97adcd0
commit a27ac010ec
2 changed files with 79 additions and 0 deletions

View file

@ -140,6 +140,7 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
*Claude Sessions:* *Claude Sessions:*
/new <name> [persona] - Create new Claude session /new <name> [persona] - Create new Claude session
/session <name> - Switch to a session /session <name> - Switch to a session
/archive <name> - Archive and remove a session
*Status & Monitoring:* *Status & Monitoring:*
/status - Quick service overview /status - Quick service overview
@ -407,6 +408,38 @@ async def switch_session_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE)
logger.error(f"Error switching session: {e}") logger.error(f"Error switching session: {e}")
await update.message.reply_text(f"Error switching session: {e}") await update.message.reply_text(f"Error switching session: {e}")
async def archive_session_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Archive a Claude session (compress and remove)."""
if not is_authorized(update.effective_user.id):
return
if not context.args:
await update.message.reply_text("Usage: /archive <name>")
return
name = context.args[0]
try:
# Terminate subprocess if running
if name in subprocesses:
if subprocesses[name].is_alive:
await subprocesses[name].terminate()
del subprocesses[name]
# Archive the session
archive_path = session_manager.archive_session(name)
size_mb = archive_path.stat().st_size / (1024 * 1024)
await update.message.reply_text(
f"Session '{name}' archived ({size_mb:.1f} MB)\n{archive_path.name}"
)
logger.info(f"Archived session '{name}' to {archive_path}")
except ValueError as e:
await update.message.reply_text(str(e))
except Exception as e:
logger.error(f"Error archiving session: {e}")
await update.message.reply_text(f"Error archiving session: {e}")
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle free text messages - route to active Claude session.""" """Handle free text messages - route to active Claude session."""
if not is_authorized(update.effective_user.id): if not is_authorized(update.effective_user.id):
@ -535,6 +568,7 @@ def main():
app.add_handler(CommandHandler("chatid", chatid)) app.add_handler(CommandHandler("chatid", chatid))
app.add_handler(CommandHandler("new", new_session)) app.add_handler(CommandHandler("new", new_session))
app.add_handler(CommandHandler("session", switch_session_cmd)) app.add_handler(CommandHandler("session", switch_session_cmd))
app.add_handler(CommandHandler("archive", archive_session_cmd))
app.add_handler(CommandHandler("status", status)) app.add_handler(CommandHandler("status", status))
app.add_handler(CommandHandler("pbs", pbs)) app.add_handler(CommandHandler("pbs", pbs))
app.add_handler(CommandHandler("pbs_status", pbs_status)) app.add_handler(CommandHandler("pbs_status", pbs_status))

View file

@ -21,6 +21,8 @@ Directory structure:
import json import json
import logging import logging
import re import re
import shutil
import subprocess
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@ -316,6 +318,49 @@ class SessionManager:
""" """
return self.base_dir / name return self.base_dir / name
def archive_session(self, name: str) -> Path:
"""
Archive a session by compressing it with tar+pigz and removing the original.
Args:
name: Session name to archive
Returns:
Path to the archive file
Raises:
ValueError: If session does not exist
"""
if not self.session_exists(name):
raise ValueError(f"Session '{name}' does not exist")
# Clear active session if archiving the active one
if self.active_session == name:
self.active_session = None
# Create archive directory
archive_dir = self.base_dir.parent / "sessions_archive"
archive_dir.mkdir(parents=True, exist_ok=True)
# Build archive filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive_name = f"{name}_{timestamp}.tar.gz"
archive_path = archive_dir / archive_name
# Compress with tar + pigz
session_dir = self.base_dir / name
subprocess.run(
["tar", "--use-compress-program=pigz", "-cf", str(archive_path),
"-C", str(self.base_dir), name],
check=True,
)
# Remove original session directory
shutil.rmtree(session_dir)
logger.info(f"Archived session '{name}' to {archive_path}")
return archive_path
def load_persona(self, name: str) -> dict: def load_persona(self, name: str) -> dict:
""" """
Load persona from library. Load persona from library.