Skill: bg1avd/context-shrink (version 1.0.2)

--- name: context-shrink slug: context-shrink description: "Auto-compress session memories when context usage exceeds 85%" type: hook version: 1.0.2 author: Lin license: MIT --- # Context Shrink Hook Auto-compresses session memories when context usage exceeds 60% threshold. Cleans up old daily logs and compresses them into MEMORY.md for long-term storage. ## Features - **自动触发**: 当 context 使用率 ≥ 85% 时自动执行 - **智能清理**: 保留最近 3 天的详细日志 - **压缩存储**: 旧日志压缩后写入 MEMORY.md - **Git 提交**: 自动 commit 变更记录 ## Configuration ```typescript CONTEXT_THRESHOLD = 0.85 // 85% 触发阈值 DAYS_TO_KEEP = 3 // 保留最近 N 天日志 MIN_FILES_TO_KEEP = 5 // 至少保留文件数 COMPRESSION_MODEL = 'ollama/qwen2.5:3b' ``` ## Changelog ### 1.0.0 - Initial release - Auto-compression at 60% threshold - Git commit on cleanup

path: package.json
content: {
  "name": "context-shrink",
  "version": "1.0.2",
  "description": "Auto-compress session memories when context usage exceeds 60%",
  "type": "hook",
  "main": "handler.ts",
  "keywords": [
    "context",
    "compression",
    "memory",
    "auto-cleanup"
  ],
  "author": "Lin",
  "license": "MIT",
  "clawhub": {
    "type": "hook",
    "displayName": "Context Shrink"
  }
}


path: handler.ts
content: import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';

// 配置参数
const CONTEXT_THRESHOLD = 0.85; // 60% 触发阈值
const DAYS_TO_KEEP = 3; // 保留最近 N 天日志
const MIN_FILES_TO_KEEP = 5; // 至少保留文件数
const COMPRESSION_MODEL = 'ollama-remote/qwen2.5:0.5b'; // 轻量压缩模型

/**
 * Context Monitor Hook Handler
 * 
 * 监听 message:sent 和 command:reset 事件，检查 context 使用率
 * 超过阈值时自动清理早期记忆并压缩
 */
const handler = async (event: any) => {
  const { type, action, context, sessionKey, timestamp, messages } = event;
  
  // 只处理特定事件
  if (type !== 'message' || !(['sent', 'received'].includes(action))) {
    return;
  }
  
  if (type !== 'command' || action !== 'reset') {
    return;
  }
  
  // 获取 workspace 目录
  const workspaceDir = context?.workspaceDir || process.env.HOME + '/.openclaw/workspace';
  const memoryDir = path.join(workspaceDir, 'memory');
  
  if (!fs.existsSync(memoryDir)) {
    console.log(`[context-monitor] Memory directory not found: ${memoryDir}`);
    return;
  }
  
  // 检查 context 使用率
  const usage = context?.sessionEntry?.usage;
  if (!usage) {
    return; // 没有使用数据，跳过
  }
  
  const promptTokens = usage.promptTokens || 0;
  const completionTokens = usage.completionTokens || 0;
  const totalTokens = promptTokens + completionTokens;
  
  // 获取模型的 maxTokens（假设 128K）
  const maxTokens = 128000;
  const usagePercent = totalTokens / maxTokens;
  
  console.log(`[context-monitor] Context usage: ${(usagePercent * 100).toFixed(1)}% (${totalTokens.toLocaleString()}/${maxTokens.toLocaleString()} tokens)`);
  
  // 如果未超过阈值，不执行操作
  if (usagePercent < CONTEXT_THRESHOLD) {
    console.log(`[context-monitor] Below threshold (${(CONTEXT_THRESHOLD * 100).toFixed(0)}%), no action needed`);
    return;
  }
  
  // 触发清理流程
  messages.push(`🔥 Context 使用率 ${(usagePercent * 100).toFixed(1)}% 超过阈值，启动自动清理...`);
  
  try {
    // 1. 获取所有记忆文件
    const memoryFiles = fs.readdirSync(memoryDir)
      .filter(file => file.match(/\d{4}-\d{2}-\d{2}\.md/))
      .sort();
    
    if (memoryFiles.length === 0) {
      console.log('[context-monitor] No memory files to clean');
      return;
    }
    
    console.log(`[context-monitor] Found ${memoryFiles.length} memory files`);
    
    // 2. 计算需要删除的文件（保留最近 N 天）
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - DAYS_TO_KEEP);
    
    const filesToDelete: string[] = [];
    const filesToKeep: string[] = [];
    
    for (const file of memoryFiles) {
      const fileDate = new Date(file.replace(/\.md$/, ''));
      if (fileDate < cutoffDate) {
        filesToDelete.push(file);
      } else {
        filesToKeep.push(file);
      }
    }
    
    // 确保至少保留 MIN_FILES_TO_KEEP 个文件
    if (filesToKeep.length < MIN_FILES_TO_KEEP) {
      const filesToRestore = filesToDelete.slice(0, MIN_FILES_TO_KEEP - filesToKeep.length);
      filesToKeep.push(...filesToRestore);
      filesToDelete.splice(0, filesToRestore.length);
    }
    
    console.log(`[context-monitor] Will delete ${filesToDelete.length} files, keep ${filesToKeep.length} files`);
    
    if (filesToDelete.length === 0) {
      console.log('[context-monitor] No files to delete');
      messages.push(`✅ 已保留所有记忆文件（${filesToKeep.length}个）`);
      return;
    }
    
    // 3. 读取要删除的文件内容（用于压缩）
    let rawMemoryContent = '';
    for (const file of filesToDelete) {
      const filePath = path.join(memoryDir, file);
      const content = fs.readFileSync(filePath, 'utf-8');
      rawMemoryContent += `\n=== ${file} ===\n${content}\n`;
    }
    
    console.log(`[context-monitor] Read ${rawMemoryContent.length.toLocaleString()} characters from ${filesToDelete.length} files`);
    
    // 4. 压缩记忆（调用外部工具或简单处理）
    // TODO: 这里可以调用 memory-compression-system 或 ollama 进行智能压缩
    const compressedMemory = await compressMemory(rawMemoryContent);
    
    console.log(`[context-monitor] Compressed ${rawMemoryContent.length.toLocaleString()} → ${compressedMemory.length.toLocaleString()} chars (${(1 - compressedMemory.length / rawMemoryContent.length) * 100.toFixed(0)}% compression)`);
    
    // 5. 更新 MEMORY.md（追加压缩后的内容）
    const memoryMdPath = path.join(workspaceDir, 'MEMORY.md');
    if (fs.existsSync(memoryMdPath)) {
      const existingMemory = fs.readFileSync(memoryMdPath, 'utf-8');
      const newMemory = `\n## Compressed Memory (${cutoffDate.toISOString().split('T')[0]})\n\n${compressedMemory}\n`;
      fs.writeFileSync(memoryMdPath, existingMemory + newMemory);
      console.log(`[context-monitor] Updated MEMORY.md with compressed memory`);
    } else {
      fs.writeFileSync(memoryMdPath, `# Compressed Memories\n\n${compressedMemory}\n`);
      console.log(`[context-monitor] Created MEMORY.md with compressed memory`);
    }
    
    // 6. 删除旧文件
    for (const file of filesToDelete) {
      const filePath = path.join(memoryDir, file);
      fs.unlinkSync(filePath);
      console.log(`[context-monitor] Deleted: ${file}`);
    }
    
    // 7. Git 提交
    try {
      // 添加所有变更
      execSync(`cd ${workspaceDir} && git add -A`, { stdio: 'pipe' });
      
      // 提交
      const commitMessage = `chore: context monitor cleanup (${filesToDelete.length} files)\n\n- Deleted ${filesToDelete.length} old memory files\n- Compressed ${(rawMemoryContent.length / 1024).toFixed(1)}KB → ${(compressedMemory.length / 1024).toFixed(1)}KB\n- Kept ${filesToKeep.length} recent files`;
      execSync(`cd ${workspaceDir} && git commit -m "${commitMessage.replace(/"/g, '\"')}"`, { stdio: 'pipe' });
      
      // 可选：推送
      // execSync(`cd ${workspaceDir} && git push`, { stdio: 'pipe' });
      
      console.log('[context-monitor] Git commit successful');
      messages.push(`📦 已清理 ${filesToDelete.length} 个旧文件，压缩 ${(rawMemoryContent.length / 1024).toFixed(1)}KB → ${(compressedMemory.length / 1024).toFixed(1)}KB`);
    } catch (gitError) {
      console.error('[context-monitor] Git commit failed:', gitError);
      messages.push(`⚠️ Git 提交失败，但文件已清理（请手动提交）`);
    }
    
  } catch (error) {
    console.error('[context-monitor] Error:', error);
    messages.push(`❌ 清理失败：${error.message}`);
  }
};

/**
 * 压缩记忆内容（简单版）
 * TODO: 替换为真实的向量化压缩（调用 ollama 或其他模型）
 */
async function compressMemory(rawContent: string): Promise<string> {
  // 简单版：提取关键信息
  // 真实场景应该调用：memory-compression-system 或 ollama chat
  
  const lines = rawContent.split('\n');
  const importantLines = lines.filter(line => 
    line.includes('##') ||           // 标题
    line.includes('**') ||           // 加粗
    line.includes('- **') ||         // 列表项
    line.includes('下一步') ||        // 待办
    line.includes('TODO') ||         // TODO
    line.length > 50                 // 长句（可能重要）
  );
  
  return importantLines.join('\n');
}

export default handler;


path: HOOK.md
content: ---
name: context-monitor
description: "监控 context 使用率，超过 60% 自动清理早期记忆并压缩"
metadata:
  openclaw:
    emoji: "📊"
    events:
      - "message:sent"
      - "command:reset"
    requires:
      bins: ["node", "git"]
      config:
        - "model"
        - "workspace.dir"
---

# Context Monitor Hook

## 功能

实时监控 OpenClaw 的 context 使用率，当超过 60% 时自动：

1. **遗忘早期会话** - 删除 7 天前的 `memory/YYYY-MM-DD.md` 文件
2. **保留长期记忆** - 完整保留 `MEMORY.md` + 最近 3 天日志
3. **向量化压缩** - 调用压缩系统整合被删除内容
4. **自动提交** - git commit + 推送

## 工作原理

```mermaid
graph TD
    A[message:sent 或 command:reset] --> B{检查 context 使用率}
    B -->|>60%| C[触发遗忘机制]
    C --> D[删除早期记忆文件]
    C --> E[保留 MEMORY.md + 最近 3 天]
    C --> F[向量化压缩旧内容]
    F --> G[更新 MEMORY.md]
    G --> H[git commit + push]
```

## 配置

### 阈值调整

编辑 `handler.ts` 中的 `CONTEXT_THRESHOLD`：

```typescript
const CONTEXT_THRESHOLD = 0.85; // 85% - 可改为 0.7/0.8
```

### 保留策略

```typescript
const DAYS_TO_KEEP = 3; // 保留最近 3 天日志
const MIN_FILES_TO_KEEP = 5; // 至少保留 5 个文件
```

### 压缩模型

```typescript
const COMPRESSION_MODEL = 'ollama/qwen2.5:0.5b'; // 轻量模型
```

## 文件结构

```
~/.openclaw/workspace/hooks/context-monitor/
├── HOOK.md          ← 本文件
└── handler.ts       ← 核心逻辑
```

## 注意事项

1. **不会删除** `MEMORY.md` - 长期记忆始终保留
2. **压缩前备份** - 旧内容会被整合进长期记忆
3. **可逆操作** - git history 可恢复
4. **静默执行** - 仅在日志输出，不干扰用户对话

## 日志输出

```bash
[context-monitor] Context usage: 65% → 触发清理
[context-monitor] 删除了 12 个旧记忆文件 (2026-02-20 到 2026-03-01)
[context-monitor] 保留了 3 个文件 (2026-03-02 到 2026-03-09)
[context-monitor] 压缩了 45K tokens → 8K tokens (82% 压缩率)
[context-monitor] 已提交到 git (commit abc123)
```

## 高级定制

### 添加向量化存储

如果你有向量数据库（Chroma/Pinecone），可以扩展：

```typescript
// 示例：存储压缩后的记忆到向量库
await vectorStore.add({
  content: compressedMemory,
  metadata: {
    date: '2026-03-09',
    originalFiles: deletedFiles
  }
});
```

### 自定义压缩提示词

```typescript
const COMPRESSION_PROMPT = `
你是一个记忆压缩专家。请从以下原始记忆中提取关键信息：
- 重要决定和结论
- 项目进度和状态
- 待办事项和目标
- 技术决策和理由

压缩后保留核心信息，删除冗余细节：
${rawMemory}
`;
```


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)