Back to skills
SkillHub ClubShip Full StackFull StackBackend

clawpressor

Compress OpenClaw session context to reduce token usage and extend session lifetime. Uses NLP summarization (Sumy) to intelligently compact conversation history while preserving essential context. Triggers on mentions of session compression, token reduction, context cleanup, or when session size exceeds safe thresholds (~300KB). Use when (1) OpenClaw approaches 50% context limit, (2) Sessions are slowing down due to large context, (3) Reducing API costs from excessive token consumption, (4) Extending session lifetime without forced reboots.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,104
Hot score
99
Updated
March 20, 2026
Overall rating
C0.0
Composite score
0.0
Best-practice grade
B70.0

Install command

npx @skill-hub/cli install openclaw-skills-clawpressor

Repository

openclaw/skills

Skill path: skills/beboxos/clawpressor

Compress OpenClaw session context to reduce token usage and extend session lifetime. Uses NLP summarization (Sumy) to intelligently compact conversation history while preserving essential context. Triggers on mentions of session compression, token reduction, context cleanup, or when session size exceeds safe thresholds (~300KB). Use when (1) OpenClaw approaches 50% context limit, (2) Sessions are slowing down due to large context, (3) Reducing API costs from excessive token consumption, (4) Extending session lifetime without forced reboots.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install clawpressor into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding clawpressor to shared team environments
  • Use clawpressor for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: clawpressor
description: Compress OpenClaw session context to reduce token usage and extend session lifetime. Uses NLP summarization (Sumy) to intelligently compact conversation history while preserving essential context. Triggers on mentions of session compression, token reduction, context cleanup, or when session size exceeds safe thresholds (~300KB). Use when (1) OpenClaw approaches 50% context limit, (2) Sessions are slowing down due to large context, (3) Reducing API costs from excessive token consumption, (4) Extending session lifetime without forced reboots.
---

# ClawPressor - Session Context Compressor

Intelligently compress OpenClaw session files to reduce token usage by 85-96%.

**Author:** JARVIS (AI Coder) | **Managed by:** BeBoX
**License:** MIT | **Version:** 1.0.0

## Quick Start

```bash
# Preview compression without changes
python3 scripts/compress.py --dry-run

# Apply compression
python3 scripts/compress.py --apply

# Restore from backup
python3 scripts/compress.py --restore
```

## When to Use

| Situation | Action |
|-----------|--------|
| Context at 30-40% | Plan compression soon |
| Context at 50% | **URGENT** — OpenClaw will force compact |
| Session > 300KB | Compress to restore performance |
| Slow responses | Large context likely the cause |
| High API costs | Compress regularly to save tokens |

## How It Works

1. **Preserves recent context** — Keeps last 5 messages intact for immediate context
2. **Summarizes old messages** — Uses LexRank algorithm to extract key information
3. **Replaces with compact block** — Single system message containing summary
4. **Creates backup** — Original preserved as `.backup` file

## Prerequisites

```bash
pip install sumy
python -c "import nltk; nltk.download('punkt_tab'); nltk.download('stopwords')"
```

## Command Reference

```bash
# Find and compress latest session (dry-run)
python3 scripts/compress.py

# Compress specific session
python3 scripts/compress.py --session /path/to/session.jsonl --apply

# Keep more recent messages (default: 5)
python3 scripts/compress.py --keep 10 --apply

# Restore if something went wrong
python3 scripts/compress.py --restore

# View compression statistics
python3 scripts/compress.py --stats
```

## Typical Results

| Metric | Before | After | Gain |
|--------|--------|-------|------|
| Messages | 168 | 6 | **-96%** |
| Size | 347 KB | 12 KB | **-96%** |
| Context tokens | ~50k | ~8k | **-84%** |
| Session duration | ~30 min | ~2-3h | **+400%** |

## Integration with Workflows

**In HEARTBEAT.md:**
```markdown
## Context Maintenance (1x/jour)
- Check session size: `ls -lh ~/.openclaw/agents/main/sessions/*.jsonl`
- If > 200KB: `python3 skills/clawpressor/scripts/compress.py --apply`
```

**Manual check:**
```bash
# See current session stats
ls -lh ~/.openclaw/agents/main/sessions/*.jsonl | head -1
```

## Safety

- Always creates `.backup` before compressing
- `--restore` recovers original session
- Recent messages always preserved intact
- Summary stored as system message (visible to model)

## Troubleshooting

| Issue | Solution |
|-------|----------|
| "Sumy not installed" | Run `pip install sumy` and NLTK downloads |
| No session found | Check `~/.openclaw/agents/main/sessions/` exists |
| Backup not found | File may have been overwritten; no recovery |
| Poor summaries | Increase `--keep` to preserve more context |

## Credits

- **Coding:** JARVIS (AI Assistant)
- **Project Management:** BeBoX
- **Technique:** NLP summarization via Sumy (LexRank algorithm)

## Related

- See `memory/openclaw-context-optimization.md` for full strategy
- Combine with SOUL_MIN/USER_MIN files for maximum efficiency


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/compress.py

```python
#!/usr/bin/env python3
"""
ClawPressor - Session Context Compressor for OpenClaw
Compresses OpenClaw session files using intelligent summarization.
Reduces token usage by ~85-96% while preserving essential context.

Usage:
    python3 compress.py --dry-run    # Preview compression
    python3 compress.py --apply      # Apply compression
    python3 compress.py --restore    # Restore from backup
"""

import json
import os
import sys
import glob
import argparse
from datetime import datetime
from pathlib import Path

# Sumy imports for intelligent summarization
try:
    from sumy.parsers.plaintext import PlaintextParser
    from sumy.nlp.tokenizers import Tokenizer
    from sumy.summarizers.lex_rank import LexRankSummarizer
    HAS_SUMY = True
except ImportError:
    HAS_SUMY = False
    print("⚠️  Sumy not installed. Falling back to truncation mode.")
    print("   Install with: pip install sumy")

# Configuration
DEFAULT_SESSION_DIR = os.path.expanduser("~/.openclaw/agents/main/sessions")
STATS_FILE = os.path.expanduser("~/.openclaw/workspace/memory/compression-stats.json")
MESSAGES_TO_KEEP = 5  # Always keep last N messages intact
SENTENCES_PER_SUMMARY = 5  # Number of sentences in summary
TOKENS_PER_CHAR = 0.25  # Rough estimate: 4 chars ≈ 1 token


class SessionCompressor:
    """Compresses OpenClaw session files using NLP summarization."""
    
    def __init__(self, lang="french"):
        self.lang = lang
        if HAS_SUMY:
            try:
                self.tokenizer = Tokenizer(lang)
                self.summarizer = LexRankSummarizer()
            except Exception as e:
                print(f"⚠️  Tokenizer error: {e}")
                print("   Using fallback mode.")
                self.tokenizer = None
                self.summarizer = None
        else:
            self.tokenizer = None
            self.summarizer = None
    
    def find_latest_session(self, session_dir=None):
        """Find the most recently modified session file."""
        if session_dir is None:
            session_dir = DEFAULT_SESSION_DIR
        
        jsonl_files = glob.glob(os.path.join(session_dir, "*.jsonl"))
        if not jsonl_files:
            return None
        
        # Sort by modification time (most recent first)
        jsonl_files.sort(key=lambda f: os.path.getmtime(f), reverse=True)
        return jsonl_files[0]
    
    def load_session(self, filepath):
        """Load session file as list of JSON objects."""
        messages = []
        with open(filepath, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                if line:
                    try:
                        msg = json.loads(line)
                        messages.append(msg)
                    except json.JSONDecodeError:
                        continue
        return messages
    
    def summarize_text(self, text, num_sentences=SENTENCES_PER_SUMMARY):
        """Summarize text using Sumy LexRank algorithm."""
        if not HAS_SUMY or not self.tokenizer or not self.summarizer:
            # Fallback: truncate
            return text[:500] + "..." if len(text) > 500 else text
        
        try:
            parser = PlaintextParser.from_string(text, self.tokenizer)
            summary = self.summarizer(parser.document, num_sentences)
            return " ".join([str(s) for s in summary])
        except Exception as e:
            # Fallback on error
            return text[:500] + "..." if len(text) > 500 else text
    
    def extract_content(self, message):
        """Extract text content from a message."""
        content = ""
        
        # Try different content fields
        if 'content' in message:
            c = message['content']
            if isinstance(c, str):
                content = c
            elif isinstance(c, list):
                # Handle content as list of objects
                for item in c:
                    if isinstance(item, dict) and 'text' in item:
                        content += item['text'] + " "
        
        # Add tool calls info if present
        if 'tool_calls' in message:
            for tc in message['tool_calls']:
                if 'function' in tc:
                    fn = tc['function']
                    content += f" [Tool: {fn.get('name', 'unknown')}]"
        
        # Add tool results
        if 'tool_call_id' in message and 'content' in message:
            content += f" [Tool Result]"
        
        return content.strip()
    
    def compress_session(self, messages, keep_last=MESSAGES_TO_KEEP):
        """
        Compress session by summarizing old messages.
        Keeps last N messages intact for immediate context.
        """
        if len(messages) <= keep_last + 2:
            return messages, 0, 0
        
        # Split messages
        to_summarize = messages[:-keep_last]
        to_keep = messages[-keep_last:]
        
        # Extract all content from old messages
        all_content = []
        role_content = {}
        
        for msg in to_summarize:
            role = msg.get('role', 'unknown')
            content = self.extract_content(msg)
            
            if content:
                if role not in role_content:
                    role_content[role] = []
                role_content[role].append(content)
        
        # Create summary for each role
        summary_parts = []
        for role in ['system', 'user', 'assistant', 'tool']:
            if role in role_content:
                role_text = " ".join(role_content[role])
                if role_text:
                    summary = self.summarize_text(role_text, SENTENCES_PER_SUMMARY)
                    summary_parts.append(f"[{role.upper()}] {summary}")
        
        # Create compacted message
        compacted_content = "\n\n".join(summary_parts)
        compacted_message = {
            "role": "system",
            "content": f"[CONTEXT COMPACTED - Previous {len(to_summarize)} messages summarized]\n\n{compacted_content}",
            "_compacted": True,
            "_original_count": len(to_summarize),
            "_timestamp": datetime.now().isoformat()
        }
        
        # Build new message list
        new_messages = [compacted_message] + to_keep
        
        return new_messages, len(messages), len(new_messages)
    
    def save_session(self, filepath, messages, backup=True, compression_stats=None):
        """Save messages back to JSONL file."""
        if backup:
            backup_path = filepath + ".backup"
            if os.path.exists(filepath):
                os.rename(filepath, backup_path)
                print(f"💾 Backup created: {backup_path}")
        
        with open(filepath, 'w', encoding='utf-8') as f:
            for msg in messages:
                f.write(json.dumps(msg, ensure_ascii=False) + '\n')
        
        # Log compression stats if provided
        if compression_stats:
            self.log_compression(
                compression_stats['original_count'],
                compression_stats['new_count'],
                compression_stats['before_stats'],
                compression_stats['after_stats']
            )
    
    def restore_backup(self, filepath):
        """Restore session from backup file."""
        backup_path = filepath + ".backup"
        if not os.path.exists(backup_path):
            print(f"❌ No backup found at {backup_path}")
            return False
        
        os.rename(backup_path, filepath)
        print(f"✅ Restored from backup: {filepath}")
        return True
    
    def get_stats(self, messages):
        """Get statistics about the session."""
        total_chars = sum(len(json.dumps(m)) for m in messages)
        roles = {}
        for m in messages:
            role = m.get('role', 'unknown')
            roles[role] = roles.get(role, 0) + 1
        
        return {
            'message_count': len(messages),
            'total_chars': total_chars,
            'total_kb': total_chars / 1024,
            'estimated_tokens': int(total_chars * TOKENS_PER_CHAR),
            'roles': roles
        }
    
    def log_compression(self, original_count, new_count, before_stats, after_stats):
        """Log compression stats to tracking file."""
        try:
            # Load existing stats
            stats = {'compressions': [], 'daily_summary': {}}
            if os.path.exists(STATS_FILE):
                with open(STATS_FILE, 'r', encoding='utf-8') as f:
                    stats = json.load(f)
            
            # Ensure directory exists
            os.makedirs(os.path.dirname(STATS_FILE), exist_ok=True)
            
            # Calculate gains
            today = datetime.now().strftime('%Y-%m-%d')
            messages_saved = original_count - new_count
            chars_saved = before_stats['total_chars'] - after_stats['total_chars']
            tokens_saved = before_stats['estimated_tokens'] - after_stats['estimated_tokens']
            
            # Log this compression
            compression_entry = {
                'timestamp': datetime.now().isoformat(),
                'date': today,
                'messages_before': original_count,
                'messages_after': new_count,
                'messages_saved': messages_saved,
                'kb_before': round(before_stats['total_kb'], 2),
                'kb_after': round(after_stats['total_kb'], 2),
                'kb_saved': round(chars_saved / 1024, 2),
                'tokens_saved': tokens_saved
            }
            stats['compressions'].append(compression_entry)
            
            # Update daily summary
            if today not in stats['daily_summary']:
                stats['daily_summary'][today] = {
                    'compressions_count': 0,
                    'total_messages_saved': 0,
                    'total_kb_saved': 0,
                    'total_tokens_saved': 0
                }
            
            daily = stats['daily_summary'][today]
            daily['compressions_count'] += 1
            daily['total_messages_saved'] += messages_saved
            daily['total_kb_saved'] += round(chars_saved / 1024, 2)
            daily['total_tokens_saved'] += tokens_saved
            
            # Save stats
            with open(STATS_FILE, 'w', encoding='utf-8') as f:
                json.dump(stats, f, indent=2, ensure_ascii=False)
            
            # Update Google Sheet if configured
            self._update_gsheet(compression_entry)
            
            return compression_entry
        except Exception as e:
            print(f"⚠️  Could not log stats: {e}")
            return None
    
    def _update_gsheet(self, compression_entry):
        """Update Google Sheet with new compression data if configured."""
        try:
            gsheets_id_file = os.path.expanduser("~/.openclaw/workspace/memory/gsheets_compression_id.txt")
            
            if not os.path.exists(gsheets_id_file):
                return  # No sheet configured, skip silently
            
            with open(gsheets_id_file, 'r') as f:
                spreadsheet_id = f.read().strip()
            
            # Import Google API
            try:
                from google.oauth2.credentials import Credentials
                from googleapiclient.discovery import build
            except ImportError:
                return  # Google libs not available
            
            # Load credentials
            creds_file = os.path.expanduser("~/.openclaw/workspace/config/google_credentials.json")
            if not os.path.exists(creds_file):
                return
            
            with open(creds_file, 'r') as f:
                creds_data = json.load(f)
            
            creds = Credentials.from_authorized_user_info(creds_data)
            service = build('sheets', 'v4', credentials=creds)
            
            # Append to details sheet
            row = [
                compression_entry['timestamp'][:19],
                compression_entry['date'],
                compression_entry['messages_before'],
                compression_entry['messages_after'],
                compression_entry['kb_before'],
                compression_entry['kb_after'],
                compression_entry['tokens_saved']
            ]
            
            service.spreadsheets().values().append(
                spreadsheetId=spreadsheet_id,
                range='Détails par Compression!A:G',
                valueInputOption='RAW',
                body={'values': [row]}
            ).execute()
            
            # Update daily summary sheet
            # First, find if this date already exists
            result = service.spreadsheets().values().get(
                spreadsheetId=spreadsheet_id,
                range='Résumé Quotidien!A:A'
            ).execute()
            
            values = result.get('values', [])
            date_row = None
            for i, row_vals in enumerate(values):
                if row_vals and row_vals[0] == compression_entry['date']:
                    date_row = i + 1
                    break
            
            if date_row:
                # Update existing row
                range_name = f'Résumé Quotidien!A{date_row}:E{date_row}'
                stats = self.get_daily_stats(days=1)
                if stats and compression_entry['date'] in stats:
                    day = stats[compression_entry['date']]
                    service.spreadsheets().values().update(
                        spreadsheetId=spreadsheet_id,
                        range=range_name,
                        valueInputOption='RAW',
                        body={'values': [[
                            compression_entry['date'],
                            day['compressions_count'],
                            day['total_messages_saved'],
                            day['total_tokens_saved'],
                            round(day['total_kb_saved'], 2)
                        ]]}
                    ).execute()
            else:
                # Append new row
                stats = self.get_daily_stats(days=1)
                if stats and compression_entry['date'] in stats:
                    day = stats[compression_entry['date']]
                    service.spreadsheets().values().append(
                        spreadsheetId=spreadsheet_id,
                        range='Résumé Quotidien!A:E',
                        valueInputOption='RAW',
                        body={'values': [[
                            compression_entry['date'],
                            day['compressions_count'],
                            day['total_messages_saved'],
                            day['total_tokens_saved'],
                            round(day['total_kb_saved'], 2)
                        ]]}
                    ).execute()
            
            print(f"   📊 Google Sheet updated")
            
        except Exception as e:
            # Silently fail - don't break compression if sheet update fails
            pass
    
    def get_daily_stats(self, days=7):
        """Get compression stats for the last N days."""
        try:
            if not os.path.exists(STATS_FILE):
                return None
            
            with open(STATS_FILE, 'r', encoding='utf-8') as f:
                stats = json.load(f)
            
            # Get last N days
            daily = stats.get('daily_summary', {})
            sorted_days = sorted(daily.keys(), reverse=True)[:days]
            
            return {day: daily[day] for day in sorted_days}
        except Exception as e:
            print(f"⚠️  Could not read stats: {e}")
            return None


def format_number(n):
    """Format large numbers with separators."""
    return f"{n:,}".replace(",", " ")


def main():
    parser = argparse.ArgumentParser(
        description="ClawPressor - Compress OpenClaw session context",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s --dry-run           # Preview compression without changes
  %(prog)s --apply             # Compress the session
  %(prog)s --restore           # Restore from backup
  %(prog)s --session FILE      # Target specific session file
  %(prog)s --stats             # Show daily compression stats
        """
    )
    parser.add_argument('--session', '-s', help='Path to specific session file')
    parser.add_argument('--dry-run', '-n', action='store_true', help='Preview only, no changes')
    parser.add_argument('--apply', '-a', action='store_true', help='Apply compression')
    parser.add_argument('--restore', '-r', action='store_true', help='Restore from backup')
    parser.add_argument('--stats', action='store_true', help='Show compression statistics')
    parser.add_argument('--keep', '-k', type=int, default=MESSAGES_TO_KEEP, 
                        help=f'Number of recent messages to keep (default: {MESSAGES_TO_KEEP})')
    
    args = parser.parse_args()
    
    compressor = SessionCompressor()
    
    # Handle stats display
    if args.stats:
        daily_stats = compressor.get_daily_stats(days=7)
        if not daily_stats:
            print("📊 No compression stats yet.")
            print(f"   Stats file: {STATS_FILE}")
            sys.exit(0)
        
        print("📈 Compression Statistics (Last 7 Days)")
        print("=" * 50)
        
        total_compressions = 0
        total_tokens_saved = 0
        total_kb_saved = 0
        
        for date, day_stats in sorted(daily_stats.items(), reverse=True):
            count = day_stats['compressions_count']
            tokens = day_stats['total_tokens_saved']
            kb = day_stats['total_kb_saved']
            
            total_compressions += count
            total_tokens_saved += tokens
            total_kb_saved += kb
            
            print(f"\n📅 {date}:")
            print(f"   Compressions: {count}")
            print(f"   Messages saved: {day_stats['total_messages_saved']}")
            print(f"   KB saved: {kb:.1f} KB")
            print(f"   Tokens saved: ~{format_number(tokens)}")
        
        print("\n" + "=" * 50)
        print(f"📊 TOTAL (7 days):")
        print(f"   {total_compressions} compressions")
        print(f"   {total_kb_saved:.1f} KB saved")
        print(f"   ~{format_number(total_tokens_saved)} tokens saved")
        print(f"   💰 Est. savings: ${total_tokens_saved * 0.000003:.2f} - ${total_tokens_saved * 0.000015:.2f}")
        sys.exit(0)
    
    # Find session file
    if args.session:
        session_file = args.session
    else:
        session_file = compressor.find_latest_session()
    
    if not session_file or not os.path.exists(session_file):
        print("❌ No session file found!")
        print(f"   Searched in: {DEFAULT_SESSION_DIR}")
        sys.exit(1)
    
    print(f"🎯 Session: {os.path.basename(session_file)}")
    print(f"   Path: {session_file}")
    
    # Handle restore
    if args.restore:
        if compressor.restore_backup(session_file):
            sys.exit(0)
        else:
            sys.exit(1)
    
    # Load session
    messages = compressor.load_session(session_file)
    
    if not messages:
        print("❌ Session file is empty!")
        sys.exit(1)
    
    # Get before stats
    before_stats = compressor.get_stats(messages)
    print(f"\n📊 Current session: {before_stats['message_count']} messages, {before_stats['total_kb']:.1f} KB")
    print(f"   Estimated tokens: ~{format_number(before_stats['estimated_tokens'])}")
    
    # Compress
    new_messages, original_count, new_count = compressor.compress_session(messages, args.keep)
    
    # Get after stats
    after_stats = compressor.get_stats(new_messages)
    reduction = (1 - new_count / original_count) * 100 if original_count > 0 else 0
    tokens_saved = before_stats['estimated_tokens'] - after_stats['estimated_tokens']
    
    print(f"\n📈 Compression preview:")
    print(f"   Messages: {original_count} → {new_count} ({reduction:.1f}% reduction)")
    print(f"   Size: {before_stats['total_kb']:.1f} KB → {after_stats['total_kb']:.1f} KB")
    print(f"   Tokens saved: ~{format_number(tokens_saved)}")
    
    if new_count < original_count:
        print(f"\n📝 Summary preview:")
        summary_msg = new_messages[0]
        preview = summary_msg.get('content', '')[:300]
        print(f"   {preview}...")
    
    # Apply or dry-run
    if args.apply:
        compression_stats = {
            'original_count': original_count,
            'new_count': new_count,
            'before_stats': before_stats,
            'after_stats': after_stats
        }
        compressor.save_session(session_file, new_messages, backup=True, compression_stats=compression_stats)
        print(f"\n✅ Compression applied!")
        print(f"   💾 Backup saved as: {session_file}.backup")
        print(f"   📊 Logged: {format_number(tokens_saved)} tokens saved")
    else:
        print(f"\n🔍 DRY RUN - No changes made")
        print(f"   Use --apply to compress the session")


if __name__ == "__main__":
    main()

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# ClawPressor

Intelligently compress OpenClaw session context to reduce token usage and extend session lifetime.

## Installation

```bash
pip install sumy
python -c "import nltk; nltk.download('punkt_tab'); nltk.download('stopwords')"
```

## Usage

```bash
# Preview
python3 scripts/compress.py --dry-run

# Apply
python3 scripts/compress.py --apply

# Restore
python3 scripts/compress.py --restore

# Stats
python3 scripts/compress.py --stats
```

## Results

- **-96%** messages
- **-84%** tokens  
- **+400%** session duration

## Credits

- **Coding:** JARVIS 🤖
- **Management:** BeBoX 👤

## License

MIT

```

### _meta.json

```json
{
  "owner": "beboxos",
  "slug": "clawpressor",
  "displayName": "Session Context Compressor",
  "latest": {
    "version": "1.0.0",
    "publishedAt": 1770937388253,
    "commit": "https://github.com/openclaw/skills/commit/c0592aceee8c96b48205091af1a768705ce02fa3"
  },
  "history": []
}

```

clawpressor | SkillHub