Skill: csl3170/context-memory-manager (version 1.0.3)

--- name: context-memory-manager description: > Agent 上下文记忆管理。每次被唤醒时自动检查 session 上下文使用率， 超过阈值时保存完整对话并提炼记忆。支持每日定时复盘已有记忆、 合并冗余、防止遗忘。 使用场景：(1) 自动监控上下文使用率 (2) 超阈值保存完整对话并压缩记忆 (3) 每日定时复盘已有记忆 (4) 查询当前 session 状态。 触发词："监控上下文"、"压缩记忆"、"复盘记忆"、"memory review"、"context monitor"、 "记忆管理"、"上下文太长"、"清理记忆"、"每日复盘"、"安装 context-memory-manager"。 --- # Context Memory Manager Agent 上下文感知记忆管理：自动保存完整对话 → 提炼结构化记忆 → 防止遗忘。 ## 架构设计 ``` ┌─────────────────────────────────────────────────────┐ │ Compress（每次被唤醒时 — 核心机制） │ │ session_status → 检查上下文使用率 │ │ → 超 70%：保存完整上下文 → 提炼记忆 → 更新 MEMORY.md │ ├─────────────────────────────────────────────────────┤ │ Review（每天凌晨 3 点 — cron 触发） │ │ 增量扫描 memory 文件 → 输出 JSON 报告 │ │ → Agent 被唤醒时检查报告 → 提炼/合并 → 更新 MEMORY.md │ └─────────────────────────────────────────────────────┘ ``` > **核心原则**：上下文检测只有 Agent 自己能做（`session_status`）， > cron 脚本仅辅助磁盘文件扫描和复盘报告产出。 ## 记忆分类 | 类型 | 路径 | 定位 | |------|------|------| | 聊天日志 | `memory/chat/YYYY-MM-DD.md` | 完整对话原始记录 | | 项目日志 | `memory/projects/<项目名>/YYYY-MM-DD.md` | 结构化沉淀 | | 核心记忆 | `MEMORY.md` | 全局索引 + 用户偏好 + 待办 | | 归档文件 | `memory/archive/` | 超过 30 天的旧日志 | **数据流向**： ``` 会话 → session_status 检测 → 超阈值 ↓ ① 保存完整上下文 → memory/chat/YYYY-MM-DD.md（原始对话，不裁剪） ↓ ② 从对话提炼 → memory/projects/<项目名>/（结构化项目记忆） ↓ ③ 更新 MEMORY.md（全局索引、用户偏好、待办事项） ↓ cron 每日扫描 → 增量对比 → 复盘提炼 → 合并冗余 ``` ## ⚡ 首次安装引导（必须执行） > 当用户安装此 skill 后首次触发时，按以下流程操作： ### Step 1：检查现有状态 ```bash ls -d <workspace>/memory/chat 2>/dev/null && echo "OK" || echo "MISSING" ls -d <workspace>/memory/projects 2>/dev/null && echo "OK" || echo "MISSING" test -f <workspace>/MEMORY.md && echo "OK" || echo "MISSING" ``` ### Step 2：提示用户 ``` 📦 context-memory-manager 已安装！需要初始化以下配置： - [ ] memory/chat/ 目录（聊天日志） - [ ] memory/projects/ 目录（项目日志） - [ ] MEMORY.md 核心记忆文件 - [ ] crontab 每日复盘任务（每天凌晨 3 点） 是否一键初始化？ ``` ### Step 3：执行初始化 1. **创建目录**：`mkdir -p <workspace>/memory/{chat,projects}` 2. **创建 MEMORY.md**（如不存在） 3. **写入 .last_review**：`date -Iseconds > <workspace>/.last_review` 4. **设置 crontab**（追加模式，不覆盖已有内容）： ``` 0 3 * * * python3 <skill_dir>/scripts/daily_review.py --workspace <workspace> --days 7 --update-timestamp --archive-days 30 > /tmp/cmm_review.log 2>&1 ``` ### Step 4：确认完成 输出初始化报告，告知用户下次被唤醒时自动开始监控上下文。 --- ## 工作流程 ### 1. 上下文压缩（每次被唤醒时自动触发） **Step 1：检查上下文使用率** ``` 调用 session_status 获取当前 session 的 token 使用量 ``` **Step 2：判断是否需要压缩** | 使用率 | 动作 | |--------|------| | < 70% | 无需操作 | | ≥ 70% | 执行压缩流程 | | ≥ 85% | 紧急压缩（优先处理） | **Step 3：保存完整上下文（必须第一步）** 调用 `sessions_history(sessionKey, limit=尽可能大)` 获取完整对话历史， 将原始对话保存为 `memory/chat/YYYY-MM-DD.md`。 **不要裁剪对话内容，保留完整记录。** **Step 4：提炼记忆** 从保存的完整对话中提炼： - **项目进展**：做了什么、改了哪些文件、遇到什么问题 - **用户偏好**：新的偏好、习惯、决策 - **待办事项**：新增/完成的待办 - **关键决策**：技术方案选择、方向调整 写入： - `memory/projects/<项目名>/YYYY-MM-DD.md`（项目记忆） - `MEMORY.md`（全局更新） **Step 5：输出摘要** 告知用户本次压缩保存了多少行对话、提炼了多少条记忆。 ### 2. 每日记忆复盘（cron 每天凌晨 3 点） **cron 脚本产出报告**： ```bash python3 scripts/daily_review.py --workspace <workspace> \ --days 7 --update-timestamp --archive-days 30 ``` 脚本输出 JSON 报告并写入 `/tmp/cmm_review_report.json`，包含： - 新增/变更的文件列表（增量扫描） - token 估算 - agent 执行指令 **Agent 被唤醒时处理报告**： 1. 检查 `/tmp/cmm_review_report.json` 是否存在 2. 如存在 → 按报告中的 `agent_instructions` 执行 3. 处理完后**删除该文件**，避免重复处理 ### 3. 自动归档 超过 30 天的聊天日志（`memory/chat/`）自动移动到 `memory/archive/`。 归档不删除文件，需要时仍可查阅。 ## ⚡ Agent 被唤醒时的自动检查 > 每次被用户消息唤醒时，按以下顺序执行： 1. **session_status 检查**：调用 `session_status` 获取上下文使用率 - ≥ 70% → 执行完整压缩流程（保存 → 提炼 → 更新 MEMORY.md） 2. **复盘报告检查**：检查 `/tmp/cmm_review_report.json` - 存在 → 读取报告，执行复盘提炼 - 处理完后删除文件 3. **如果两者都不需要** → 正常响应用户请求 ## 注意事项 - 压缩时必须**先保存完整对话**，再提炼，不允许直接裁剪丢弃 - 聊天日志保留原始对话轨迹，不精简 - 项目日志按项目分目录，结构化沉淀 - `.last_review` 文件记录复盘时间戳，用于增量扫描 - 首次复盘无 `.last_review` 文件时，全量扫描 - Token 估算为粗略值（字节数 / 2.5），实际因模型而异

path: CHANGELOG.md
content: # Changelog - context-memory-manager

## v1.2.0 (2026-04-14)

### 🔄 架构重构
- **核心机制改为 `session_status` 检测**：每次被唤醒时自动检查上下文使用率，不再依赖磁盘文件大小判断
- **移除 `monitor_context.py`**：扫磁盘 KB 大小无法反映真实上下文使用量
- **压缩前先保存完整对话**：`sessions_history` → `memory/chat/`（不裁剪），再提炼项目记忆

### ✨ 新功能
- 唤醒时自动检查流程：
  1. `session_status` → 上下文 ≥ 70% 执行压缩
  2. 保存完整对话到 `memory/chat/YYYY-MM-DD.md`
  3. 提炼项目记忆 → `memory/projects/<项目名>/`
  4. 更新 `MEMORY.md`
- 新增首次安装引导：Agent 自动设置 crontab 复盘任务

### 🗑️ 移除
- `monitor_context.py` 脚本（无意义的磁盘大小扫描）
- crontab 中的每 2 小时监控任务
- `--auto-truncate` 自动裁剪功能（不应丢弃用户对话）

### 📝 文档
- SKILL.md 完全重写，以 `session_status` 为核心
- 架构说明图更新为两步流程（Compress + Review）

---

## v1.1.0 (2026-04-14)

### 🐛 Bug 修复
- 修复 `daily_review.py` 增量过滤的时区比较报错（naive vs aware datetime）
- 修复 `.last_review` 时间戳写入失败问题

### ✨ 新功能
- cron 报告持久化到 `/tmp/cmm_review_report.json`
- Agent 唤醒时自动检查并处理积压报告

---

## v1.0.0 (2026-04-13)

### 首次发布
- 多 Agent 上下文监控 + 智能压缩 + 每日复盘
- cron 每 2 小时扫描 memory 文件磁盘占用
- cron 每天凌晨 3 点增量复盘
- 自动归档旧文件（默认 30 天）


path: references/model-contexts.md
content: # Model Context Windows

已知模型的上下文窗口大小（token 数）。用于判断是否需要压缩记忆。

## 常见模型

| 模型 | 最大上下文 | 建议压缩阈值 (70%) |
|------|-----------|-------------------|
| qwen3.6-plus | 200,000 | 140,000 |
| qwen3.5-plus | 200,000 | 140,000 |
| qwen-plus | 32,000 | 22,400 |
| qwen-turbo | 8,000 | 5,600 |
| gpt-4o | 128,000 | 89,600 |
| gpt-4o-mini | 128,000 | 89,600 |
| claude-sonnet-4 | 200,000 | 140,000 |
| claude-haiku | 200,000 | 140,000 |

## 获取模型上下文

在 agent 环境中，通过 `session_status` 工具查看：
```
📚 Context: 45k/200k (22.5%)
```
分母即为该模型的最大上下文窗口。

## 阈值计算

```
警告线    = max_context × 0.50
压缩线    = max_context × 0.70
紧急线    = max_context × 0.85
```

## 注意事项

- 不同 provider 对同一模型可能有不同的上下文限制
- 实际可用上下文可能略低于标称值（系统消息占用）
- 图片、文件附件也会占用上下文空间


path: scripts/daily_review.py
content: #!/usr/bin/env python3
"""
Daily Memory Review - Incremental Scanner
扫描 workspace 中的记忆文件，做增量对比，输出结构化报告供 agent 执行复盘。

用法:
    python3 daily_review.py --workspace /path/to/workspace [--days 7] [--dry-run]
                            [--archive-days 30]

输出 JSON 格式，包含：
- 上次复盘时间
- 新增/变更的文件列表（增量）
- 文件分类统计
- token 估算（帮助 agent 判断复盘消耗）
- agent 执行指令
"""

import argparse
import json
import os
import shutil
import sys
from datetime import datetime, timedelta


def read_last_review(workspace):
    """读取上次复盘时间戳"""
    last_review_file = os.path.join(workspace, ".last_review")
    if os.path.exists(last_review_file):
        with open(last_review_file, "r") as f:
            return f.read().strip()
    return None


def write_last_review(workspace):
    """写入本次复盘时间戳"""
    last_review_file = os.path.join(workspace, ".last_review")
    with open(last_review_file, "w") as f:
        f.write(datetime.now().isoformat())


def scan_memory_files(workspace, days=7):
    """扫描记忆目录，分类文件"""
    memory_base = os.path.join(workspace, "memory")
    chat_dir = os.path.join(memory_base, "chat")
    project_dir = os.path.join(memory_base, "projects")

    result = {
        "chat_files": [],
        "project_files": [],
        "memory_md": None,
    }

    # 扫描聊天日志
    if os.path.exists(chat_dir):
        for fname in sorted(os.listdir(chat_dir)):
            if not fname.endswith(".md"):
                continue
            fpath = os.path.join(chat_dir, fname)
            stat = os.stat(fpath)
            result["chat_files"].append({
                "file": fname,
                "path": fpath,
                "mtime": datetime.fromtimestamp(stat.st_mtime).isoformat(),
                "size_bytes": stat.st_size,
                "size_kb": round(stat.st_size / 1024, 1),
            })

    # 扫描项目日志
    if os.path.exists(project_dir):
        for project_name in sorted(os.listdir(project_dir)):
            project_path = os.path.join(project_dir, project_name)
            if not os.path.isdir(project_path):
                continue
            for fname in sorted(os.listdir(project_path)):
                if not fname.endswith(".md"):
                    continue
                fpath = os.path.join(project_path, fname)
                stat = os.stat(fpath)
                result["project_files"].append({
                    "project": project_name,
                    "file": fname,
                    "path": fpath,
                    "mtime": datetime.fromtimestamp(stat.st_mtime).isoformat(),
                    "size_bytes": stat.st_size,
                    "size_kb": round(stat.st_size / 1024, 1),
                })

    # MEMORY.md
    memory_md = os.path.join(workspace, "MEMORY.md")
    if os.path.exists(memory_md):
        stat = os.stat(memory_md)
        result["memory_md"] = {
            "path": memory_md,
            "size_bytes": stat.st_size,
            "size_kb": round(stat.st_size / 1024, 1),
            "mtime": datetime.fromtimestamp(stat.st_mtime).isoformat(),
        }

    return result


def filter_incremental(files, since_iso=None):
    """根据时间戳过滤出新增/变更的文件"""
    if not since_iso:
        return files  # 首次复盘，返回全部

    try:
        since = datetime.fromisoformat(since_iso)
        # 统一去掉时区信息，避免 naive/aware 比较报错
        if since.tzinfo is not None:
            since = since.replace(tzinfo=None)
    except ValueError:
        return files  # 解析失败，返回全部

    result = []
    for f in files:
        try:
            ftime = datetime.fromisoformat(f["mtime"])
            if ftime.tzinfo is not None:
                ftime = ftime.replace(tzinfo=None)
            if ftime > since:
                result.append(f)
        except (ValueError, KeyError):
            continue
    return result


def estimate_tokens(byte_count):
    """
    根据字节数估算 token 量。
    中文约 1.5-2 字节/token，英文约 4 字节/token。
    Markdown 混合内容取保守值 2.5 字节/token。
    """
    return round(byte_count / 2.5)


def archive_old_files(workspace, archive_days=30):
    """
    将超过指定天数的聊天日志归档到 memory/archive/ 目录。
    """
    memory_base = os.path.join(workspace, "memory")
    chat_dir = os.path.join(memory_base, "chat")
    archive_dir = os.path.join(memory_base, "archive")

    if not os.path.exists(chat_dir):
        return {"archived": [], "total_archived_kb": 0}

    cutoff = datetime.now() - timedelta(days=archive_days)
    archived = []
    total_archived_bytes = 0

    for fname in sorted(os.listdir(chat_dir)):
        if not fname.endswith(".md"):
            continue
        fpath = os.path.join(chat_dir, fname)
        mtime = datetime.fromtimestamp(os.stat(fpath).st_mtime)
        if mtime < cutoff:
            os.makedirs(archive_dir, exist_ok=True)
            archive_path = os.path.join(archive_dir, fname)
            shutil.move(fpath, archive_path)
            stat = os.stat(archive_path)
            archived.append({
                "file": fname,
                "size_kb": round(stat.st_size / 1024, 1),
                "archived_to": archive_path,
            })
            total_archived_bytes += stat.st_size

    return {
        "archived": archived,
        "total_archived_kb": round(total_archived_bytes / 1024, 1),
    }


def main():
    parser = argparse.ArgumentParser(description="每日记忆复盘 - 增量扫描器")
    parser.add_argument("--workspace", type=str, required=True,
                        help="Agent workspace 路径")
    parser.add_argument("--days", type=int, default=7,
                        help="扫描最近 N 天的文件 (default: 7)")
    parser.add_argument("--dry-run", action="store_true",
                        help="仅扫描，不执行修改")
    parser.add_argument("--update-timestamp", action="store_true",
                        help="扫描后更新 .last_review 时间戳")
    parser.add_argument("--archive-days", type=int, default=30,
                        help="超过 N 天的聊天日志自动归档 (default: 30, 0=禁用)")
    args = parser.parse_args()

    workspace = os.path.expanduser(args.workspace)
    if not os.path.isdir(workspace):
        print(json.dumps({
            "error": f"workspace 不存在: {workspace}",
            "timestamp": datetime.now().isoformat()
        }, indent=2, ensure_ascii=False))
        sys.exit(1)

    # 上次复盘时间
    last_review = read_last_review(workspace)

    # 扫描所有文件
    all_files = scan_memory_files(workspace, args.days)

    # 增量过滤
    incremental_chat = filter_incremental(all_files["chat_files"], last_review)
    incremental_projects = filter_incremental(all_files["project_files"], last_review)

    # 分类统计
    stats = {
        "total_chat": len(all_files["chat_files"]),
        "new_chat": len(incremental_chat),
        "total_projects": len(all_files["project_files"]),
        "new_projects": len(incremental_projects),
    }

    # Token 估算（帮助 agent 判断复盘会消耗多少上下文）
    incremental_chat_bytes = sum(f["size_bytes"] for f in incremental_chat)
    incremental_project_bytes = sum(f["size_bytes"] for f in incremental_projects)
    memory_md_bytes = all_files["memory_md"]["size_bytes"] if all_files["memory_md"] else 0
    total_review_bytes = incremental_chat_bytes + incremental_project_bytes + memory_md_bytes

    report = {
        "timestamp": datetime.now().isoformat(),
        "workspace": workspace,
        "mode": "dry_run" if args.dry_run else "full_review",
        "last_review": last_review,
        "is_first_review": last_review is None,
        "statistics": stats,
        "incremental": {
            "chat_files": incremental_chat,
            "project_files": incremental_projects,
        },
        "all_files": all_files,
        "token_estimate": {
            "description": "复盘需读取的文件总 token 估算",
            "formula": "字节数 / 2.5（中文 Markdown 混合内容保守值）",
            "chat_bytes": incremental_chat_bytes,
            "chat_tokens": estimate_tokens(incremental_chat_bytes),
            "project_bytes": incremental_project_bytes,
            "project_tokens": estimate_tokens(incremental_project_bytes),
            "memory_md_bytes": memory_md_bytes,
            "memory_md_tokens": estimate_tokens(memory_md_bytes),
            "total_bytes": total_review_bytes,
            "total_tokens": estimate_tokens(total_review_bytes),
            "note": (
                "此 token 量是 agent 复盘时需要读取的输入量。"
                "如 total_tokens 超过模型上下文窗口的 30%，建议分批处理。"
            ),
        },
        "agent_instructions": {
            "step1": "读取 MEMORY.md 作为基线（如存在）",
            "step2": "读取 incremental.chat_files 中的聊天日志",
            "step3": "读取 incremental.project_files 中的项目日志",
            "step4": "分析并提炼：用户偏好、决策、待办、项目进展",
            "step5": "更新 MEMORY.md（用户偏好 + 项目索引）",
            "step6": "更新/创建项目日志（如有新的沉淀内容）",
            "step7": "输出复盘摘要报告",
            "step8": f"{'跳过' if args.dry_run else '执行'} --update-timestamp 写入 .last_review"
        },
    }

    # 归档旧文件
    if args.archive_days > 0 and not args.dry_run:
        archive_result = archive_old_files(workspace, args.archive_days)
        report["archive"] = archive_result

    # 更新 .last_review（如果要求）
    if args.update_timestamp and not args.dry_run:
        write_last_review(workspace)
        report["last_review_updated"] = True

    # 输出到 stdout（供 cron 日志捕获）
    print(json.dumps(report, indent=2, ensure_ascii=False))

    # 持久化报告到磁盘，供 Agent 被唤醒时检查处理
    report_dir = "/tmp"
    report_path = os.path.join(report_dir, "cmm_review_report.json")
    with open(report_path, "w", encoding="utf-8") as f:
        json.dump(report, f, indent=2, ensure_ascii=False)


if __name__ == "__main__":
    main()


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