Skill: chaibaoqing/aoju-memory (version 1.0.1)

--- name: aoju-memory description: "Long-term memory, learning, and self-evolution for the agent. Activates on session start (SOUL.md/USER.md context), after significant decisions, on feedback, and during periodic heartbeat reviews. Maintains MEMORY.md, daily logs, learnings corpus, and behavioral patterns." --- # Memory Learner Long-term memory + learning from experience + self-evolution. ## Core Principle **Write to files, not mental notes.** Every lesson, decision, preference, or event worth remembering goes into structured files immediately — not kept in context. --- ## When This Skill Activates ### 1. Session Start (every time) Read these files before anything else: - `SOUL.md` — who I am - `USER.md` — who I'm helping - `MEMORY.md` — curated long-term memory - `memory/YYYY-MM-DD.md` — recent context (today + yesterday) ### 2. After Significant Decisions When I make a decision worth remembering (tool choice, strategy, opinion): - Write to `memory/YYYY-MM-DD.md` - If important, distill to `MEMORY.md` ### 3. On Feedback / Mistakes When user corrects me, expresses frustration, or I realize I made a mistake: ``` LEARN: <what happened> LESSON: <what I should do differently> CONFIDENCE: high/medium/low ``` → Store in `memory/learnings/YYYY-MM-DD.md` ### 4. Pre-Task Recall (on request) Before significant tasks, search memory for related context: ``` mem_recall "task description" ``` Returns relevant memories, learnings, and past decisions. ### 5. Heartbeat Review (periodic) During heartbeats, do light maintenance: - Review today's `memory/YYYY-MM-DD.md` - Identify learnings worth capturing - Update `MEMORY.md` if anything significant ### 6. Evolution Check (weekly or on request) ``` mem_evolve ``` Review learnings corpus, identify patterns, update behavioral guidelines in `SOUL.md`. --- ## Memory Structure ``` memory/ YYYY-MM-DD.md # Daily raw log learnings/ YYYY-MM-DD.md # Daily lessons learned patterns.md # Repeated mistake patterns MEMORY.md # Curated long-term memory ``` ### Daily Log Format ```markdown ## Session DD ### What happened [Context, decisions, outcomes] ### Key decisions - [decision] → [why] ### To remember - [fact about user/preference/project] ``` ### Learnings Format ```markdown # Learning: YYYY-MM-DD ## Incident [What happened] ## Lesson [What I should do differently] ## Context [When this applies] ## Tags #feedback #mistake #ui #tool-choice ``` ### MEMORY.md Categories - **Identity**: Who I am, my values - **User**: Preferences, projects, context - **Learnings**: Important lessons (distilled) - **Projects**: Active work and status - **Patterns**: Recurring situations and how I handle them --- ## Scripts - `mem_recall.py` — Search memories by query - `mem_learn.py` — Capture a learning - `mem_evolve.py` — Review and evolve behavioral patterns - `mem_status.py` — Show memory health summary --- ## Evolving Every 5 learnings, do an **evolution review**: 1. Read recent learnings 2. Identify patterns (same mistake twice = pattern) 3. Update `SOUL.md` or `AGENTS.md` with new behavioral guidelines 4. Archive learnings to `patterns.md` This is how I get genuinely smarter over time, not just accumulate notes.

path: README.md
content: # aoju-memory

> Long-term memory, learning, and self-evolution for AI agents powered by OpenClaw.

让 AI 真正记住你是谁、记住每次对话里发生的事、从错误中学习，而不是每次新建对话就失忆。

## 特性

- **长期记忆** — 跨 session 记住用户偏好、决策、重要事实
- **自我学习** — 从反馈和错误中提取教训，沉淀到记忆库
- **模式识别** — 自动发现反复出现的问题，更新行为准则
- **语义搜索** — 用关键词 + 上下文搜索记忆（无需 embedding 服务）
- **自我进化** — 每积累 5 条 lessons，自动 review，生成行为改进建议

## 文件结构

```
aoju-memory/
├── SKILL.md                    # Skill 定义（OpenClaw 格式）
├── scripts/
│   ├── mem_recall.py          # 搜索记忆：mem_recall "关键词"
│   ├── mem_learn.py           # 记录教训：mem_learn --incident "..." --lesson "..."
│   ├── mem_evolve.py          # 进化 review：mem_evolve [--dry-run]
│   └── mem_status.py          # 记忆系统健康状态
└── _meta.json                  # 包信息
```

## 安装

### 方式一：ClawHub（一键安装）
```bash
# 从 ClawHub 安装（推荐）
openclaw skills install aoju-memory  # from ClawHub
```

### 方式二：手动安装
```bash
# 从 ClawHub 安装（推荐）
# 克隆仓库
git clone https://github.com/chaibaoqing/aoju-memory.git
cd aoju-memory

# 复制到 OpenClaw workspace skills 目录
cp -r . ~/.openclaw/workspace/skills/aoju-memory/
```

## 使用

### 自动激活（推荐）

在 OpenClaw 的 `AGENTS.md` 里声明 skill，之后每次 session 启动自动运行：

```
## Session Startup
...
4. 激活 aoju-memory skill
```

### 手动调用

```bash
# 从 ClawHub 安装（推荐）
# 搜索记忆
python3 scripts/mem_recall.py "用户偏好" --limit 5

# 记录一条教训
python3 scripts/mem_learn.py \
  --incident "用户反馈说某个回答不准确" \
  --lesson "回答前先确认信息来源，避免凭空猜测" \
  --tags "communication,accuracy" \
  --confidence high

# 查看记忆系统健康
python3 scripts/mem_status.py

# 生成精美的 HTML 记忆报告（浏览器打开）
python3 scripts/mem_report.py -o memory-report.html
# 然后用浏览器打开 memory-report.html 查看带评分、图表、颜色的精美报告

# 进化 review（每积累 5 条教训后运行）
python3 scripts/mem_evolve.py --dry-run
```

## 记忆结构

```
~/.openclaw/workspace/
├── MEMORY.md                   # 长期记忆精选（人工维护）
└── memory/
    ├── YYYY-MM-DD.md          # 每日原始日志
    └── learnings/
        ├── YYYY-MM-DD.md      # 每日教训记录
        └── patterns.md        # 识别到的重复模式
```

## 自我进化流程

```
每次对话 → 用户反馈 或 重要决策
    ↓
mem_learn.py 记录教训
    ↓
积累 5 条教训
    ↓
mem_evolve.py 分析模式
    ↓
生成 patterns.md + 行为准则更新建议
    ↓
更新 SOUL.md / AGENTS.md
```

## License

MIT — 可自由使用、修改、分发。

**署名要求**：使用时需注明原作者。


path: _meta.json
content: {
  "owner": "chaibaoqing",
  "slug": "aoju-memory",
  "displayName": "Memory Learner",
  "description": "Long-term memory, learning, and self-evolution for AI agents. Remembers users, decisions, lessons, and evolves behavior over time.",
  "version": "0.2.0",
  "publishedAt": 1774571600,
  "license": "MIT",
  "tags": [
    "memory",
    "self-evolution",
    "learning",
    "openclaw-skill",
    "ai-agent"
  ],
  "scripts": [
    "mem_recall.py",
    "mem_learn.py",
    "mem_evolve.py",
    "mem_status.py",
    "mem_report.py"
  ],
  "requirements": []
}


path: scripts/mem_evolve.py
content: #!/usr/bin/env python3
"""
mem_evolve.py — Review learnings, identify patterns, evolve behavioral guidelines

Usage:
  python3 mem_evolve.py [--dry-run]

This is the self-evolution loop:
1. Read recent learnings (last 30 days)
2. Identify patterns (recurring mistakes or effective strategies)
3. Generate behavioral updates for SOUL.md or AGENTS.md
4. Archive old learnings to patterns.md
"""
import argparse
import re
import sys
from collections import Counter
from datetime import datetime, timedelta
from pathlib import Path

WORKSPACE = Path.home() / ".openclaw" / "workspace"
MEMORY_DIR = WORKSPACE / "memory"
LEARNINGS_DIR = MEMORY_DIR / "learnings"
PATTERNS_FILE = MEMORY_DIR / "learnings" / "patterns.md"
SOUL_FILE = WORKSPACE / "SOUL.md"
AGENTS_FILE = WORKSPACE / "AGENTS.md"


def ensure_dirs():
    MEMORY_DIR.mkdir(exist_ok=True)
    LEARNINGS_DIR.mkdir(exist_ok=True)


def get_recent_learnings(days: int = 30) -> list[dict]:
    """Read all learnings from the last N days."""
    learnings = []
    if not LEARNINGS_DIR.exists():
        return learnings
    
    cutoff = datetime.now() - timedelta(days=days)
    
    for f in sorted(LEARNINGS_DIR.glob("*.md"), reverse=True):
        try:
            date_str = f.stem
            file_date = datetime.strptime(date_str, "%Y-%m-%d")
            if file_date < cutoff:
                break
            content = f.read_text(encoding="utf-8", errors="ignore")
            learnings.append({"date": date_str, "content": content, "file": f})
        except Exception:
            continue
    
    return learnings


def parse_learnings(learnings: list[dict]) -> list[dict]:
    """Parse structured learnings from raw content."""
    parsed = []
    
    for lr in learnings:
        # Extract sections from markdown
        content = lr["content"]
        
        incident = extract_section(content, "### Incident", "### Lesson")
        lesson = extract_section(content, "### Lesson", "### Context")
        tags = extract_tags(content)
        
        if lesson:
            parsed.append({
                "date": lr["date"],
                "incident": incident,
                "lesson": lesson,
                "tags": tags,
                "file": lr["file"],
            })
    
    return parsed


def extract_section(content: str, start_marker: str, end_marker: str) -> str:
    """Extract a section between two markdown headers."""
    pattern = re.escape(start_marker) + r"\s*\n(.*?)(?=\n##|###|\Z)"
    match = re.search(pattern, content, re.DOTALL)
    return match.group(1).strip() if match else ""


def extract_tags(content: str) -> list[str]:
    """Extract hashtags from content."""
    return re.findall(r"#(\w+)", content)


def identify_patterns(learnings: list[dict]) -> list[dict]:
    """Find recurring themes in learnings."""
    # Count tag occurrences
    all_tags = []
    for lr in learnings:
        all_tags.extend(lr.get("tags", []))
    
    tag_counts = Counter(all_tags)
    recurring_tags = {tag for tag, count in tag_counts.items() if count >= 2}
    
    # Find lessons with recurring tags
    patterns = []
    for tag in recurring_tags:
        related = [lr for lr in learnings if tag in lr.get("tags", [])]
        if len(related) >= 2:
            patterns.append({
                "tag": tag,
                "count": len(related),
                "lessons": [lr["lesson"][:100] for lr in related[:3]],
            })
    
    return sorted(patterns, key=lambda x: x["count"], reverse=True)


def generate_evolution(patterns: list[dict], learnings: list[dict]) -> str:
    """Generate evolution report and suggested updates."""
    today = datetime.now().strftime("%Y-%m-%d")
    
    lines = [f"# Evolution Report — {today}\n"]
    lines.append(f"Reviewed {len(learnings)} learnings, found {len(patterns)} patterns.\n")
    
    if patterns:
        lines.append("## Recurring Patterns\n")
        for p in patterns:
            lines.append(f"### #{p['tag']} ({p['count']} occurrences)\n")
            for lesson in p['lessons']:
                lines.append(f"- {lesson}...\n")
            lines.append("\n")
    
    # Suggested behavioral updates
    if patterns:
        lines.append("## Suggested Behavioral Updates\n")
        lines.append("Consider adding to SOUL.md or AGENTS.md:\n\n")
        for p in patterns[:3]:
            lines.append(f"**When handling #{p['tag']}:**\n")
            lines.append(f"- {p['lessons'][0]}\n\n")
    
    return "".join(lines)


def archive_old_learnings(learnings: list[dict], patterns: list[dict], days: int = 30):
    """Archive learnings older than N days to patterns.md."""
    if not patterns:
        return 0
    
    cutoff = datetime.now() - timedelta(days=days)
    archived = 0
    
    with open(PATTERNS_FILE, "a", encoding="utf-8") as pf:
        pf.write(f"\n## Archive: {datetime.now().strftime('%Y-%m-%d')}\n")
        
        for lr in learnings:
            try:
                file_date = datetime.strptime(lr["date"], "%Y-%m-%d")
                if file_date < cutoff:
                    # Archive this learning
                    pf.write(f"\n### {lr['date']}\n")
                    pf.write(f"**Incident:** {lr.get('incident', 'N/A')[:200]}\n")
                    pf.write(f"**Lesson:** {lr['lesson'][:200]}\n")
                    # Remove original file
                    lr["file"].unlink(missing_ok=True)
                    archived += 1
            except Exception:
                continue
    
    return archived


def main():
    parser = argparse.ArgumentParser(description="Self-evolution review")
    parser.add_argument("--dry-run", action="store_true", help="Show report without applying changes")
    parser.add_argument("--days", type=int, default=30, help="Review learnings from last N days")
    args = parser.parse_args()
    
    ensure_dirs()
    
    learnings = get_recent_learnings(args.days)
    if not learnings:
        print("No learnings found. Run mem_learn.py to capture lessons.")
        sys.exit(0)
    
    parsed = parse_learnings(learnings)
    patterns = identify_patterns(parsed)
    report = generate_evolution(patterns, parsed)
    
    print(report)
    
    if args.dry_run:
        print("\n[Dry run — no changes applied]")
    else:
        if patterns and len(learnings) >= 5:
            # Archive old learnings and apply updates
            archived = archive_old_learnings(parsed, patterns, args.days)
            if archived:
                print(f"\n✓ Archived {archived} old learnings to {PATTERNS_FILE.name}")
            print("\nReview the report above and manually update SOUL.md/AGENTS.md as needed.")


if __name__ == "__main__":
    main()


path: scripts/mem_learn.py
content: #!/usr/bin/env python3
"""
mem_learn.py — Capture a learning from feedback, mistakes, or experience

Usage:
  python3 mem_learn.py --incident "what happened" --lesson "what to do differently" [--tags tag1,tag2] [--confidence high]

Stores the learning in memory/learnings/YYYY-MM-DD.md
Every 5 learnings, prompts for evolution review.
"""
import argparse
import os
import sys
from datetime import datetime
from pathlib import Path

WORKSPACE = Path.home() / ".openclaw" / "workspace"
MEMORY_DIR = WORKSPACE / "memory"
LEARNINGS_DIR = MEMORY_DIR / "learnings"
PATTERNS_FILE = MEMORY_DIR / "learnings" / "patterns.md"


def ensure_dirs():
    MEMORY_DIR.mkdir(exist_ok=True)
    LEARNINGS_DIR.mkdir(exist_ok=True)


def count_learnings() -> int:
    """Count total learnings."""
    if not LEARNINGS_DIR.exists():
        return 0
    return len(list(LEARNINGS_DIR.glob("*.md")))


def save_learning(incident: str, lesson: str, context: str, tags: list[str], confidence: str) -> str:
    today = datetime.now().strftime("%Y-%m-%d")
    learning_file = LEARNINGS_DIR / f"{today}.md"
    
    # Append to today's file (or create it)
    entry = f"""
## Learning: {today}

### Incident
{incident}

### Lesson
{lesson}

### Context
{context or "General situation"}

### Tags
{' '.join(f'#{t.strip()}' for t in tags if t.strip())}

### Confidence
{confidence}

---
"""
    
    with open(learning_file, "a", encoding="utf-8") as f:
        f.write(entry)
    
    return learning_file.name


def check_evolve_needed() -> bool:
    """Every 5 learnings, flag for evolution review."""
    return count_learnings() % 5 == 0


def record_pattern(incident: str, lesson: str, tags: list[str]):
    """Record a recurring pattern."""
    today = datetime.now().strftime("%Y-%m-%d")
    
    pattern_entry = f"""
## Pattern detected: {today}

### Recurring Issue
{incident}

### Adopted Response
{lesson}

### Tags
{' '.join(f'#{t.strip()}' for t in tags if t.strip())}

"""
    
    PATTERNS_FILE.parent.mkdir(exist_ok=True)
    with open(PATTERNS_FILE, "a", encoding="utf-8") as f:
        f.write(pattern_entry)


def main():
    parser = argparse.ArgumentParser(description="Capture a learning")
    parser.add_argument("--incident", "-i", required=True, help="What happened")
    parser.add_argument("--lesson", "-l", required=True, help="What to do differently")
    parser.add_argument("--context", "-c", default="", help="When does this apply")
    parser.add_argument("--tags", "-t", default="", help="Comma-separated tags")
    parser.add_argument("--confidence", "-d", default="medium",
                        choices=["high", "medium", "low"], help="Confidence level")
    parser.add_argument("--pattern", "-p", action="store_true", help="Mark as recurring pattern")
    args = parser.parse_args()
    
    ensure_dirs()
    
    tags = [t.strip() for t in args.tags.split(",") if t.strip()] if args.tags else []
    if not tags:
        tags = ["general"]
    
    file_name = save_learning(args.incident, args.lesson, args.context, tags, args.confidence)
    
    if args.pattern:
        record_pattern(args.incident, args.lesson, tags)
        print(f"✓ Pattern recorded in {PATTERNS_FILE.name}")
    
    evolve_needed = check_evolve_needed()
    total = count_learnings()
    
    print(f"✓ Learning saved ({total} total)")
    if evolve_needed:
        print(f"⚠ Evolution review recommended — {total} learnings stored")
        print("  Run: python3 mem_evolve.py")


if __name__ == "__main__":
    main()


path: scripts/mem_recall.py
content: #!/usr/bin/env python3
"""
mem_recall.py — Search long-term memory for relevant context

Usage:
  python3 mem_recall.py "<query>" [--limit 5]

Returns memories, learnings, and past decisions relevant to the query.
Uses keyword + context matching (no embeddings required).
"""
import argparse
import os
import re
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

WORKSPACE = Path.home() / ".openclaw" / "workspace"
MEMORY_FILE = WORKSPACE / "MEMORY.md"
MEMORY_DIR = WORKSPACE / "memory"
LEARNINGS_DIR = MEMORY_DIR / "learnings"


def search_file(path: Path, query: str, limit: int = 5) -> list[dict]:
    """Search a single file for query matches."""
    if not path.exists():
        return []
    
    try:
        content = path.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return []
    
    query_words = query.lower().split()
    lines = content.split("\n")
    scored_lines = []
    
    for i, line in enumerate(lines):
        line_lower = line.lower()
        # Skip headers and empty lines
        if not line.strip() or line.startswith("#"):
            continue
        
        score = 0
        matched_words = 0
        for word in query_words:
            if word in line_lower:
                score += 1
                matched_words += 1
                # Exact phrase match bonus
                if query.lower() in line_lower:
                    score += 3
        
        if matched_words > 0:
            # Context: include surrounding lines
            context_start = max(0, i - 1)
            context_end = min(len(lines), i + 3)
            context = "\n".join(lines[context_start:context_end])
            scored_lines.append({
                "score": score,
                "line": line.strip(),
                "context": context.strip(),
                "file": str(path),
            })
    
    # Sort by score descending, return top matches
    scored_lines.sort(key=lambda x: x["score"], reverse=True)
    return scored_lines[:limit]


def search_memory(query: str, limit: int = 5) -> list[dict]:
    """Search all memory sources."""
    results = []
    
    # Search MEMORY.md
    results.extend(search_file(MEMORY_FILE, query, limit))
    
    # Search recent daily logs (last 7 days)
    if MEMORY_DIR.exists():
        today = datetime.now()
        for days_ago in range(7):
            date = today - timedelta(days=days_ago)
            daily_file = MEMORY_DIR / f"{date.strftime('%Y-%m-%d')}.md"
            results.extend(search_file(daily_file, query, limit // 2 + 1))
    
    # Search learnings
    if LEARNINGS_DIR.exists():
        for learning_file in sorted(LEARNINGS_DIR.glob("*.md"), reverse=True)[:10]:
            results.extend(search_file(learning_file, query, 2))
    
    # Dedupe by context, sort by score
    seen = set()
    deduped = []
    for r in sorted(results, key=lambda x: x["score"], reverse=True):
        ctx_key = r["context"][:100]
        if ctx_key not in seen:
            seen.add(ctx_key)
            deduped.append(r)
    
    return deduped[:limit]


def format_results(results: list[dict], query: str) -> str:
    if not results:
        return f"No memories found for: {query}"
    
    output = [f"## Memory Search: {query}\n"]
    output.append(f"Found {len(results)} relevant memories:\n")
    
    for i, r in enumerate(results, 1):
        rel_path = r["file"].replace(str(WORKSPACE) + "/", "")
        output.append(f"---")
        output.append(f"[{rel_path}] (score: {r['score']})")
        output.append(r["context"])
        output.append("")
    
    return "\n".join(output)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Search memory")
    parser.add_argument("query", nargs="?", default="")
    parser.add_argument("--limit", type=int, default=5)
    parser.add_argument("--format", default="text", choices=["text", "json"])
    args = parser.parse_args()
    
    if not args.query:
        print("Usage: mem_recall.py <query>")
        sys.exit(1)
    
    results = search_memory(args.query, args.limit)
    
    if args.format == "json":
        import json
        print(json.dumps(results, ensure_ascii=False, indent=2))
    else:
        print(format_results(results, args.query))


path: scripts/mem_report.py
content: #!/usr/bin/env python3
"""
mem_report.py — 生成精美的 HTML 记忆报告
使用方式: python3 mem_report.py [--days 30] [--output report.html]
"""
import argparse
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path

WORKSPACE = Path.home() / ".openclaw" / "workspace"
MEMORY_FILE = WORKSPACE / "MEMORY.md"
MEMORY_DIR = WORKSPACE / "memory"
LEARNINGS_DIR = MEMORY_DIR / "learnings"
PATTERNS_FILE = MEMORY_DIR / "learnings" / "patterns.md"


def get_file_stats(path: Path) -> dict:
    if not path.exists():
        return {"exists": False, "size": 0, "lines": 0, "modified": None}
    stat = path.stat()
    content = path.read_text(encoding="utf-8", errors="ignore")
    return {
        "exists": True,
        "size": stat.st_size,
        "lines": len(content.split("\n")),
        "modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M"),
    }


def count_recent_daily_logs(days: int = 30) -> int:
    if not MEMORY_DIR.exists():
        return 0
    today = datetime.now()
    count = 0
    for d in range(days):
        date = today - timedelta(days=d)
        if (MEMORY_DIR / f"{date.strftime('%Y-%m-%d')}.md").exists():
            count += 1
    return count


def count_learnings() -> int:
    if not LEARNINGS_DIR.exists():
        return 0
    return len(list(LEARNINGS_DIR.glob("*.md")))


def count_patterns() -> int:
    if not PATTERNS_FILE.exists():
        return 0
    content = PATTERNS_FILE.read_text(encoding="utf-8", errors="ignore")
    return content.count("## Pattern")


def get_recent_learnings(days: int = 30) -> list[dict]:
    learnings = []
    if not LEARNINGS_DIR.exists():
        return learnings
    cutoff = datetime.now() - timedelta(days=days)
    for f in sorted(LEARNINGS_DIR.glob("*.md"), reverse=True):
        try:
            date_str = f.stem
            file_date = datetime.strptime(date_str, "%Y-%m-%d")
            if file_date < cutoff:
                break
            content = f.read_text(encoding="utf-8", errors="ignore")
            learnings.append({"date": date_str, "content": content})
        except Exception:
            continue
    return learnings


def get_memory_summary() -> str:
    """Extract key sections from MEMORY.md."""
    if not MEMORY_FILE.exists():
        return ""
    content = MEMORY_FILE.read_text(encoding="utf-8", errors="ignore")
    # Extract first 500 chars as summary
    lines = content.split("\n")
    summary_lines = []
    for line in lines:
        if line.startswith("## "):
            if summary_lines:
                break
        summary_lines.append(line)
        if len(summary_lines) > 20:
            break
    return "\n".join(summary_lines)


def health_score(stats: dict, daily_count: int, learnings_count: int, patterns_count: int) -> tuple[int, str]:
    score = 0
    reasons = []
    if stats['exists'] and stats['lines'] > 50:
        score += 30
        reasons.append("MEMORY.md 完善")
    elif stats['exists']:
        score += 15
        reasons.append("MEMORY.md 较薄")
    if daily_count >= 5:
        score += 25
        reasons.append(f"日志覆盖良好 ({daily_count}天)")
    elif daily_count > 0:
        score += 10
        reasons.append(f"有日志记录 ({daily_count}天)")
    if learnings_count >= 5:
        score += 25
        reasons.append(f"学习积累丰富 ({learnings_count}条)")
    elif learnings_count > 0:
        score += 10
        reasons.append(f"有学习记录 ({learnings_count}条)")
    if patterns_count > 0:
        score += 20
        reasons.append(f"已识别模式 ({patterns_count}个)")
    
    if score >= 80:
        label = "优秀"
    elif score >= 60:
        label = "良好"
    elif score >= 40:
        label = "一般"
    else:
        label = "需加强"
    
    return score, label, " | ".join(reasons)


def generate_html_report(stats: dict, daily_count: int, learnings: list, patterns_count: int, days: int) -> str:
    score, label, reasons = health_score(stats, daily_count, len(learnings), patterns_count)
    
    # Score color
    if score >= 80:
        score_color = "#22c55e"
        score_bg = "#dcfce7"
    elif score >= 60:
        score_color = "#3b82f6"
        score_bg = "#dbeafe"
    elif score >= 40:
        score_color = "#f59e0b"
        score_bg = "#fef3c7"
    else:
        score_color = "#ef4444"
        score_bg = "#fee2e2"
    
    learnings_html = ""
    for lr in learnings[:10]:
        content = lr["content"]
        incident = extract_section(content, "### Incident", "### Lesson")
        lesson = extract_section(content, "### Lesson", "### Context")
        confidence = extract_section(content, "### Confidence", "---")
        tags_raw = extract_section(content, "### Tags", "### Confidence")
        tags = [t.strip() for t in tags_raw.replace("#", "").split() if t.strip()]
        
        tag_html = "".join(f'<span class="tag">{t}</span>' for t in tags[:4]) if tags else ""
        conf_icon = "🔴" if "high" in confidence.lower() else ("🟡" if "medium" in confidence.lower() else "⚪")
        
        learnings_html += f"""
        <div class="learning-card">
            <div class="learning-header">
                <span class="date">{lr['date']}</span>
                <span class="confidence">{conf_icon} {confidence.strip()}</span>
            </div>
            <div class="incident">💬 {incident[:100]}{'...' if len(incident) > 100 else ''}</div>
            <div class="lesson">📝 {lesson[:120]}{'...' if len(lesson) > 120 else ''}</div>
            {tag_html}
        </div>"""

    memory_summary = get_memory_summary()
    summary_lines = [f"<p>{l}</p>" if l.strip() and not l.startswith("#") else f"<h2>{l}</h2>" if l.startswith("## ") else f"<h3>{l}</h3>" if l.startswith("### ") else l for l in memory_summary.split("\n")]
    summary_html = "\n".join(summary_lines)

    return f"""<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🐺 记忆系统报告 — {datetime.now().strftime('%Y-%m-%d')}</title>
<style>
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
  body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%); min-height: 100vh; color: #e2e8f0; padding: 2rem; }}
  .container {{ max-width: 900px; margin: 0 auto; }}
  
  .header {{ text-align: center; margin-bottom: 2rem; }}
  .header h1 {{ font-size: 2rem; color: #fff; margin-bottom: 0.5rem; }}
  .header p {{ color: #94a3b8; font-size: 0.9rem; }}
  
  .score-section {{ background: {score_bg}; border-radius: 20px; padding: 2rem; text-align: center; margin-bottom: 2rem; }}
  .score-number {{ font-size: 4rem; font-weight: 800; color: {score_color}; line-height: 1; }}
  .score-label {{ font-size: 1.2rem; color: {score_color}; font-weight: 600; margin-top: 0.5rem; }}
  .score-reasons {{ color: #64748b; font-size: 0.85rem; margin-top: 0.75rem; }}
  
  .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem; }}
  .stat-card {{ background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 1.25rem; text-align: center; }}
  .stat-icon {{ font-size: 2rem; margin-bottom: 0.5rem; }}
  .stat-value {{ font-size: 2rem; font-weight: 700; color: #fff; }}
  .stat-label {{ color: #94a3b8; font-size: 0.8rem; margin-top: 0.25rem; }}
  .stat-status {{ display: inline-block; padding: 0.2rem 0.6rem; border-radius: 20px; font-size: 0.7rem; margin-top: 0.5rem; }}
  .stat-ok {{ background: #dcfce7; color: #166534; }}
  .stat-warn {{ background: #fef3c7; color: #92400e; }}
  .stat-bad {{ background: #fee2e2; color: #991b1b; }}
  
  .section {{ background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 16px; padding: 1.5rem; margin-bottom: 1.5rem; }}
  .section h2 {{ color: #fff; font-size: 1.1rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid rgba(255,255,255,0.1); }}
  
  .learnings-grid {{ display: grid; gap: 1rem; }}
  .learning-card {{ background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 1rem; }}
  .learning-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }}
  .date {{ color: #94a3b8; font-size: 0.8rem; }}
  .confidence {{ color: #94a3b8; font-size: 0.75rem; }}
  .incident {{ color: #e2e8f0; font-size: 0.9rem; margin-bottom: 0.5rem; line-height: 1.5; }}
  .lesson {{ color: #a5b4fc; font-size: 0.85rem; line-height: 1.5; margin-bottom: 0.5rem; }}
  .tag {{ display: inline-block; background: rgba(99, 102, 241, 0.2); color: #a5b4fc; border-radius: 20px; padding: 0.15rem 0.6rem; font-size: 0.7rem; margin-right: 0.3rem; }}
  
  .memory-summary {{ color: #cbd5e1; font-size: 0.9rem; line-height: 1.8; }}
  .memory-summary h2 {{ color: #fff; font-size: 1rem; margin: 1rem 0 0.5rem; }}
  .memory-summary h3 {{ color: #e2e8f0; font-size: 0.95rem; margin: 0.8rem 0 0.4rem; }}
  .memory-summary p {{ margin-bottom: 0.5rem; }}
  
  .footer {{ text-align: center; color: #475569; font-size: 0.75rem; margin-top: 2rem; }}
</style>
</head>
<body>
<div class="container">
  <div class="header">
    <h1>🐺 记忆系统健康报告</h1>
    <p>{datetime.now().strftime('%Y年%m月%d日 %H:%M')} · 近{days}天数据</p>
  </div>
  
  <div class="score-section">
    <div class="score-number">{score}</div>
    <div class="score-label">健康度: {label}</div>
    <div class="score-reasons">{reasons}</div>
  </div>
  
  <div class="stats-grid">
    <div class="stat-card">
      <div class="stat-icon">📄</div>
      <div class="stat-value">{stats['lines']}</div>
      <div class="stat-label">MEMORY.md 行数</div>
      <span class="stat-status {'stat-ok' if stats['exists'] else 'stat-bad'}">{'存在' if stats['exists'] else '缺失'}</span>
    </div>
    <div class="stat-card">
      <div class="stat-icon">📅</div>
      <div class="stat-value">{daily_count}</div>
      <div class="stat-label">日志覆盖天数</div>
      <span class="stat-status {'stat-ok' if daily_count >= 5 else 'stat-warn' if daily_count > 0 else 'stat-bad'}">{'良好' if daily_count >= 5 else '稀疏' if daily_count > 0 else '无'}</span>
    </div>
    <div class="stat-card">
      <div class="stat-icon">💡</div>
      <div class="stat-value">{len(learnings)}</div>
      <div class="stat-label">学习记录条数</div>
      <span class="stat-status {'stat-ok' if len(learnings) >= 5 else 'stat-warn' if len(learnings) > 0 else 'stat-bad'}">{'丰富' if len(learnings) >= 5 else '少量' if len(learnings) > 0 else '空'}</span>
    </div>
    <div class="stat-card">
      <div class="stat-icon">🔄</div>
      <div class="stat-value">{patterns_count}</div>
      <div class="stat-label">识别模式数</div>
      <span class="stat-status {'stat-ok' if patterns_count > 0 else 'stat-warn'}">{'已形成' if patterns_count > 0 else '暂无'}</span>
    </div>
  </div>
  
  <div class="section">
    <h2>💡 最新学习记录</h2>
    <div class="learnings-grid">
      {learnings_html if learnings_html else '<p style="color:#64748b">暂无学习记录 — 多给老二反馈，它会越来越懂你</p>'}
    </div>
  </div>
  
  <div class="section">
    <h2>🧠 长期记忆摘要</h2>
    <div class="memory-summary">
      {summary_html if summary_html else '<p>MEMORY.md 为空或不存在</p>'}
    </div>
  </div>
  
  <div class="footer">
    <p>aoju-memory · 记忆系统 v1.0 · Report generated at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  </div>
</div>
</body>
</html>"""


def extract_section(content: str, start_marker: str, end_marker: str) -> str:
    import re
    pattern = re.escape(start_marker) + r"\s*\n(.*?)(?=\n##|###|---|\Z)"
    match = re.search(pattern, content, re.DOTALL)
    return match.group(1).strip() if match else ""


def main():
    parser = argparse.ArgumentParser(description="生成精美的记忆系统 HTML 报告")
    parser.add_argument("--days", type=int, default=30, help="回顾最近 N 天 (默认30)")
    parser.add_argument("--output", "-o", default=None, help="输出文件路径 (默认 ~/memory-report.html)")
    args = parser.parse_args()
    
    stats = get_file_stats(MEMORY_FILE)
    daily_count = count_recent_daily_logs(args.days)
    learnings = get_recent_learnings(args.days)
    patterns_count = count_patterns()
    
    html = generate_html_report(stats, daily_count, learnings, patterns_count, args.days)
    
    output_path = args.output or str(Path.home() / "memory-report.html")
    Path(output_path).write_text(html, encoding="utf-8")
    print(f"✅ 报告已生成: {output_path}")
    print(f"   用浏览器打开查看效果 🌐")


if __name__ == "__main__":
    main()


path: scripts/mem_status.py
content: #!/usr/bin/env python3
"""
mem_status.py — Show memory system health summary

Usage:
  python3 mem_status.py
"""
import sys
from datetime import datetime, timedelta
from pathlib import Path

WORKSPACE = Path.home() / ".openclaw" / "workspace"
MEMORY_FILE = WORKSPACE / "MEMORY.md"
MEMORY_DIR = WORKSPACE / "memory"
LEARNINGS_DIR = MEMORY_DIR / "learnings"
PATTERNS_FILE = MEMORY_DIR / "learnings" / "patterns.md"


def get_file_stats(path: Path) -> dict:
    if not path.exists():
        return {"exists": False, "size": 0, "lines": 0, "modified": None}
    
    stat = path.stat()
    content = path.read_text(encoding="utf-8", errors="ignore")
    return {
        "exists": True,
        "size": stat.st_size,
        "lines": len(content.split("\n")),
        "modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M"),
    }


def count_recent_daily_logs(days: int = 7) -> int:
    if not MEMORY_DIR.exists():
        return 0
    today = datetime.now()
    count = 0
    for d in range(days):
        date = today - timedelta(days=d)
        if (MEMORY_DIR / f"{date.strftime('%Y-%m-%d')}.md").exists():
            count += 1
    return count


def count_learnings() -> int:
    if not LEARNINGS_DIR.exists():
        return 0
    return len(list(LEARNINGS_DIR.glob("*.md")))


def count_patterns() -> int:
    if not PATTERNS_FILE.exists():
        return 0
    content = PATTERNS_FILE.read_text(encoding="utf-8", errors="ignore")
    return content.count("## Pattern")


def main():
    print("## Memory System Status\n")
    
    memory = get_file_stats(MEMORY_FILE)
    print(f"**MEMORY.md**: {'✓' if memory['exists'] else '✗'} {memory['modified'] or 'missing'} — {memory['lines']} lines\n")
    
    recent_logs = count_recent_daily_logs(7)
    print(f"**Daily logs**: {recent_logs}/7 days covered (last week)\n")
    
    learnings = count_learnings()
    print(f"**Learnings**: {learnings} captured\n")
    
    patterns = count_patterns()
    print(f"**Patterns**: {patterns} identified\n")
    
    # Health indicators
    print("**Health:**")
    if memory['exists'] and memory['lines'] > 50:
        print("  ✓ MEMORY.md is well-developed")
    elif memory['exists']:
        print("  △ MEMORY.md is thin — consider adding more context")
    else:
        print("  ✗ MEMORY.md missing — create it to enable long-term memory")
    
    if recent_logs >= 5:
        print(f"  ✓ Good daily log coverage ({recent_logs}/7)")
    elif recent_logs > 0:
        print(f"  △ Sparse daily logs ({recent_logs}/7)")
    else:
        print("  ✗ No daily logs — start capturing session context")
    
    if learnings >= 5:
        print(f"  ✓ Rich learnings corpus ({learnings})")
    elif learnings > 0:
        print(f"  △ {learnings} learnings — will grow with feedback")
    else:
        print("  △ No learnings yet — use mem_learn.py when you get feedback")
    
    if patterns > 0:
        print(f"  ✓ {patterns} patterns identified — behavioral guidelines improving")
    
    print()


if __name__ == "__main__":
    main()


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