Skill: jochenyang/open-memory-system (version 1.0.0)

--- name: open-memory-system description: 三层记忆系统 — Working/Short-Term/Long-Term Memory 管理。支持自动偏好记忆、实体记忆、事件记录、L2自动提炼、Hook自动触发、Cron定期整理。用于记忆用户偏好、跨会话积累知识、自动化记忆管理。 license: MIT --- # Open Memory System 基于 OpenViking + Microsoft Agent Memory 的三层记忆框架，适用于 AI Agent 的持久化记忆系统。 ## 核心概念 | 层级 | 用途 | 存储 | |------|------|------| | **Working Memory** | 当前会话临时信息 | `working.json` | | **Short-Term Memory** | 会话级别短期记忆 | `short-term/*.md` | | **Long-Term Memory** | 偏好、实体、事件持久化 | `user/preferences/`, `user/entities/`, `user/events/` | ## 目录结构 ``` memory/ # 记忆主目录 ├── working.json # Working Memory ├── short-term/ # Short-Term Memory (会话级别) │ └── YYYY-MM-DD-HHMM.md ├── user/ # Long-Term Memory (用户视角) │ ├── preferences/ # 偏好记录 │ ├── entities/ # 实体记忆 │ └── events/ # 重要事件 └── agent/ # Agent 自身记忆 ├── persona/ # 人设/角色设定 └── episodic/ # 经验教训 ``` ## 快速安装 ### Step 1: 解压 skill 到 workspace ```bash cd ~/.openclaw/workspace/skills unzip open-memory-system.zip -d open-memory-system ``` ### Step 2: 初始化记忆目录 ```bash export MEMORY_DIR=~/.openclaw/workspace/memory mkdir -p $MEMORY_DIR/{user/{preferences,entities,events},agent/{persona,episodic},short-term} ``` ### Step 3: 部署 Hook ```bash # auto-save-memory: 部署到 ~/.openclaw/hooks/ cp -r open-memory-system/scripts/auto-save-memory ~/.openclaw/hooks/ # load-memory-on-start 已在 ~/.openclaw/hooks/ 中预装，无需重复部署 ``` ### Step 4: 创建 Cron（定时任务） 参考 `crons/memory-crons.txt` 创建每日定时任务。 ## CLI 命令 ```bash # 读取核心记忆 python3 scripts/memory.py # 每日统计 python3 scripts/memory.py summary # 清理过期 python3 scripts/memory.py cleanup # L2 提炼（从 short-term → long-term events） python3 scripts/distill_l2.py # 记录偏好 python3 scripts/memory.py pref "沟通方式" "直接高效" "用户偏好" # 记录事件 python3 scripts/memory.py event "项目启动" "Miloya 正式成立" # 记录经验 python3 scripts/memory.py episode "第一次部署" "negative" "需要检查依赖版本" ``` ## 自动化流程 ``` 用户对话 ↓ [session:end Hook] → auto-save-memory → 保存 .learnings → memory ↓ [memory-check cron] → 清理过期 + 会话摘要 → short-term ↓ [distill_l2 cron @20:00] → short-term 提炼 → user/events/ (L2) ↓ [session:start Hook] → load-memory-on-start → 加载 MEMORY.md + short-term ``` ## 内置 Hook 说明 | Hook | 位置 | 触发 | 功能 | |------|------|------|------| | `load-memory-on-start` | `~/.openclaw/hooks/`（预装） | `agent:bootstrap` | 运行 `memory.py read` 加载记忆 | | `auto-save-memory` | `~/.openclaw/hooks/`（需部署） | `session:end` | 保存 `.learnings/` 到 events | ## 自定义配置 修改 `scripts/memory.py` 开头的配置区： ```python MEMORY_DIR = Path(os.environ.get("MEMORY_DIR", "/root/.openclaw/workspace/memory")) DEFAULT_EXPIRE_DAYS = 90 ```

path: README.md
content: # Open Memory System

三层记忆框架 — 适用于 AI Agent 的持久化记忆系统。

## 功能特性

- **Working Memory** — 会话级临时记忆
- **Short-Term Memory** — 跨会话短期记忆
- **Long-Term Memory** — 持久化记忆（偏好/实体/事件）
- **自动过期清理** — 避免记忆无限膨胀
- **Hook 自动触发** — 自动捕捉用户偏好和重要事件
- **Cron 定期整理** — 保持记忆系统健康

## 安装

```bash
# 1. 解压到 skills 目录
cd ~/.openclaw/workspace/skills
unzip open-memory-system.zip -d open-memory-system

# 2. 部署 Hook 到 ~/.openclaw/hooks/
cp -r open-memory-system/scripts/auto-save-memory ~/.openclaw/hooks/
# load-memory-on-start 已在 ~/.openclaw/hooks/ 中预装，无需重复部署

# 3. 初始化记忆目录
mkdir -p ~/.openclaw/workspace/memory/{user/{preferences,entities,events},agent/{persona,episodic},short-term}

# 4. 创建 Cron（参考 crons/memory-crons.txt）
```

## 使用示例

```bash
# 记录用户偏好
python3 scripts/memory.py pref "沟通方式" "直接高效" "用户明确表示"

# 记录重要事件
python3 scripts/memory.py event "项目启动" "Miloya 开发公司正式成立" "成功"

# 记录经验教训
python3 scripts/memory.py episode "第一次部署失败" "negative" "需要检查依赖版本"

# 读取核心记忆
python3 scripts/memory.py read

# 每日统计
python3 scripts/memory.py summary
```

## 项目结构

```
open-memory-system/
├── SKILL.md              # Skill 说明文档
├── README.md             # 本文件
├── scripts/
│   ├── memory.py         # 核心记忆 CLI
│   ├── distill_l2.py     # L2 自动提炼
│   └── auto-save-memory/ # Hook: session end → 保存 learnings
│       ├── handler.js
│       └── HOOK.md
└── crons/
    └── memory-crons.txt  # Cron 配置指南
```

## 已有内置 Hook

`load-memory-on-start` 已在 `~/.openclaw/hooks/` 中预装，会话启动时自动运行 `memory/memory.py read` 加载记忆，无需额外配置。

## 设计说明

- `auto-save-memory` 在 `session:end` 时将 `.learnings/` 中的 md 文件保存到 `memory/user/events/`
- `distill_l2.py` 在每日 20:00 从 `short-term/` 提炼有价值内容到 `user/events/`

## License

MIT


path: scripts/distill_l2.py
content: #!/usr/bin/env python3
"""
L2 Distill Script - 从 short-term memory 自动提炼 L2 事件
遍历最近 N 个 short-term 摘要，用规则判断是否需要提炼到 L2，
如果满足条件则调用 memory.py event 记录。
"""

import subprocess
import sys
import os
from pathlib import Path
from datetime import datetime, timedelta

MEMORY_DIR = Path(__file__).parent.parent / "memory"
SHORT_TERM_DIR = MEMORY_DIR / "short-term"
EVENTS_DIR = MEMORY_DIR / "user" / "events"

# L2 提炼关键词（满足任一即提炼）
DISTILL_KEYWORDS = [
    "新发现", "新洞察", "重要信息",
    "重要决策", "关键选择", "架构变更", "架构",
    "踩坑", "教训", "错误", "bug",
    "偏好变化", "技术突破", "大模型",
    "Hook", "bootstrap", "记忆系统",
    "提炼", "修复", "升级", "重大",
    "TypeScript", "文档更新", "文档升级",
    "session", "conversations", "记忆加载",
    "飞书", "Bot", "插件", "多账号",
    "复盘", "晚间复盘", "日总结",
]


def get_recent_short_terms(hours=12):
    """获取最近N小时的 short-term 摘要"""
    if not SHORT_TERM_DIR.exists():
        return []
    
    cutoff = datetime.now() - timedelta(hours=hours)
    files = []
    
    for f in SHORT_TERM_DIR.glob("*.md"):
        mtime = datetime.fromtimestamp(f.stat().st_mtime)
        if mtime > cutoff:
            files.append((mtime, f))
    
    files.sort(key=lambda x: x[0], reverse=True)
    return [f for _, f in files]


def has_substantive_dialogue(content: str) -> bool:
    """检查是否有实质对话内容"""
    # 有"参与者"或多个"话题"段落说明有实质对话
    dialogue_markers = content.count("参与者") + content.count("话题")
    return dialogue_markers >= 2 or ("参与者" in content and "关键结论" in content)


def extract_dialogue_content(content: str) -> str:
    """提取对话的核心内容"""
    lines = content.split("\n")
    result = []
    capture = False
    
    for line in lines:
        if "实质性对话摘要" in line:
            capture = True
            continue
        elif capture and line.startswith("## "):
            break
        elif capture:
            stripped = line.strip()
            if not stripped:
                continue
            clean = stripped.strip("* ").strip()
            if clean:
                result.append(clean)
    
    return "\n".join(result[:20])


def should_distill(file_path: Path, content: str) -> tuple[bool, str]:
    """
    判断内容是否值得提炼到 L2
    """
    # 如果没有实质对话，直接跳过
    if not has_substantive_dialogue(content):
        return False, ""
    
    # 检查关键词匹配
    matched = []
    for kw in DISTILL_KEYWORDS:
        if kw in content:
            matched.append(kw)
    
    if matched:
        reason = f"匹配关键词: {', '.join(matched)}"
        return True, reason
    
    # 检查是否有明确的结论性内容
    if "关键结论" in content or "重要结论" in content:
        if len(content) > 800:
            return True, "有实质对话 + 关键结论"
    
    return False, ""


def event_exists_today(topic_hint: str = "") -> bool:
    """检查今天是否已有相关主题的事件"""
    if not EVENTS_DIR.exists():
        return False
    
    today = datetime.now().strftime("%Y-%m-%d")
    for f in EVENTS_DIR.glob(f"{today}*.md"):
        if not topic_hint:
            return True
        if topic_hint.lower() in f.read_text(encoding="utf-8").lower():
            return True
    return False


def record_event(title: str, description: str) -> bool:
    """调用 memory.py event 记录事件"""
    cmd = [
        sys.executable,
        str(MEMORY_DIR / "memory.py"),
        "event",
        title,
        description
    ]
    
    env = dict(os.environ)
    env["MEMORY_DIR"] = str(MEMORY_DIR)
    
    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        cwd=str(MEMORY_DIR.parent),
        env=env
    )
    
    if result.returncode == 0:
        print(f"  ✅ 已记录: {title}")
        return True
    else:
        print(f"  ❌ 失败: {result.stderr[:200]}")
        return False


def distill_one(file_path: Path) -> bool:
    """处理单个 short-term 文件"""
    content = file_path.read_text(encoding="utf-8")
    
    should, reason = should_distill(file_path, content)
    
    if not should:
        return False
    
    print(f"  ✅ 提炼条件: {reason}")
    
    # 提取对话内容
    dialogue = extract_dialogue_content(content)
    
    # 生成事件标题
    today = datetime.now().strftime("%Y-%m-%d")
    topic = "重要对话"
    
    for kw in DISTILL_KEYWORDS:
        if kw in content and len(kw) > 2:
            topic = kw
            break
    
    title = f"{today}-{topic}"
    
    # 防止重复
    if event_exists_today(topic):
        print(f"  ⏭️  今天已有同类事件，跳过")
        return False
    
    # 构造描述
    description = f"""来源: {file_path.name}

{dialogue}

提炼原因: {reason}
"""
    
    return record_event(title, description)


def main():
    print("=" * 50)
    print(f"🧠 L2 Distill 检查 - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
    print("=" * 50)
    
    recent_files = get_recent_short_terms(hours=12)
    
    if not recent_files:
        print("\n📭 最近12小时无 short-term 摘要")
        return
    
    print(f"\n📋 检查 {len(recent_files)} 个 recent short-term 文件...")
    
    distilled_count = 0
    for f in recent_files:
        print(f"\n处理: {f.name}")
        result = distill_one(f)
        if result:
            distilled_count += 1
    
    print(f"\n{'='*50}")
    if distilled_count > 0:
        print(f"✅ L2 提炼完成: 新增 {distilled_count} 个事件")
    else:
        print(f"⏭️  无需提炼（最近对话无 L2 价值内容）")


if __name__ == "__main__":
    main()


path: scripts/memory.py
content: #!/usr/bin/env python3
"""
Open Memory System v3.1
Based on OpenViking + Microsoft Agent Memory + agent-memory
支持: Working/Short-Term/Long-Term/Persona/Episodic/Entity Memory
新增: 自动过期、版本管理
"""

import os
import sys
import json
from pathlib import Path
from datetime import datetime, timedelta

# 配置
MEMORY_DIR = Path(os.environ.get("MEMORY_DIR", "/root/.openclaw/workspace/memory"))
CONFIG = {
    "working": "working.json",
    "short": "short-term/",
    "long": {
        "preferences": "user/preferences/",
        "entities": "user/entities/",
        "events": "user/events/",
    },
    "persona": "agent/persona/",
    "episodic": "agent/episodic/",
}

# 默认过期天数
DEFAULT_EXPIRE_DAYS = 90


def ensure_dir(path: Path):
    path.mkdir(parents=True, exist_ok=True)


# ========== Working Memory ==========

def set_working_memory(key: str, value: str):
    working_file = MEMORY_DIR / CONFIG["working"]
    data = {}
    if working_file.exists():
        data = json.loads(working_file.read_text())
    data[key] = {"value": value, "updated": datetime.now().isoformat()}
    working_file.write_text(json.dumps(data, ensure_ascii=False, indent=2))
    print(f"💾 Working: {key}")


def get_working_memory(key: str = None) -> dict:
    working_file = MEMORY_DIR / CONFIG["working"]
    if not working_file.exists():
        return {}
    data = json.loads(working_file.read_text())
    if key:
        return data.get(key, {})
    return data


# ========== Short Term Memory ==========

def save_short_term(session_id: str, content: str):
    short_dir = MEMORY_DIR / CONFIG["short"]
    ensure_dir(short_dir)
    session_file = short_dir / f"{session_id}.md"
    existing = ""
    if session_file.exists():
        existing = session_file.read_text() + "\n\n---\n\n"
    session_file.write_text(f"{existing}{datetime.now().strftime('%H:%M')} - {content}")


def get_short_term(session_id: str) -> str:
    short_dir = MEMORY_DIR / CONFIG["short"]
    session_file = short_dir / f"{session_id}.md"
    if session_file.exists():
        return session_file.read_text()
    return ""


# ========== Long Term Memory ==========

def record_preference(key: str, value: str, reason: str = "", expires_days: int = None):
    """记录偏好 (支持过期)"""
    pref_dir = MEMORY_DIR / CONFIG["long"]["preferences"]
    ensure_dir(pref_dir)
    
    # 过期时间
    expires = None
    if expires_days:
        expires = (datetime.now() + timedelta(days=expires_days)).strftime('%Y-%m-%d')
    elif DEFAULT_EXPIRE_DAYS:
        expires = (datetime.now() + timedelta(days=DEFAULT_EXPIRE_DAYS)).strftime('%Y-%m-%d')
    
    pref_file = pref_dir / f"{key}.md"
    content = f"""# {key}

**值**: {value}
**时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
**原因**: {reason}
**过期**: {expires or "永不过期"}

## 详情
{value}
"""
    pref_file.write_text(content, encoding="utf-8")
    update_abstract(CONFIG["long"]["preferences"])
    print(f"✅ 偏好: {key}")


def record_entity(name: str, description: str, entity_type: str = "person", attributes: dict = None):
    """记录实体"""
    entity_dir = MEMORY_DIR / CONFIG["long"]["entities"]
    ensure_dir(entity_dir)
    
    # 属性转 JSON 字符串
    attr_str = ""
    if attributes:
        attr_str = "\n**属性**: " + json.dumps(attributes, ensure_ascii=False)
    
    entity_file = entity_dir / f"{name}.md"
    content = f"""# {name}

**类型**: {entity_type}
**时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}

## 描述
{description}
{attr_str}
"""
    entity_file.write_text(content, encoding="utf-8")
    update_abstract(CONFIG["long"]["entities"])
    print(f"✅ 实体: {name} ({entity_type})")


def record_event(title: str, description: str = "", outcome: str = None):
    """记录事件 (支持标记结果)"""
    date = datetime.now().strftime('%Y-%m-%d')
    event_dir = MEMORY_DIR / CONFIG["long"]["events"]
    ensure_dir(event_dir)
    event_file = event_dir / f"{date}.md"
    
    existing = ""
    if event_file.exists():
        existing = event_file.read_text(encoding="utf-8") + "\n\n---\n\n"
    
    outcome_str = f"\n**结果**: {outcome}" if outcome else ""
    
    content = f"""{existing}## {title}

**时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}{outcome_str}

### 描述
{description}
"""
    event_file.write_text(content, encoding="utf-8")
    update_abstract(CONFIG["long"]["events"])
    print(f"✅ 事件: {title}")


# ========== Persona Memory ==========

def set_persona(key: str, value: str):
    persona_dir = MEMORY_DIR / CONFIG["persona"]
    ensure_dir(persona_dir)
    persona_file = persona_dir / f"{key}.md"
    content = f"""# {key}

**时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}

## 内容
{value}
"""
    persona_file.write_text(content, encoding="utf-8")
    print(f"✅ Persona: {key}")


def get_persona(key: str = None) -> dict:
    persona_dir = MEMORY_DIR / CONFIG["persona"]
    if key:
        persona_file = persona_dir / f"{key}.md"
        if persona_file.exists():
            return {"content": persona_file.read_text()}
        return {}
    result = {}
    for f in persona_dir.glob("*.md"):
        result[f.stem] = f.read_text()
    return result


# ========== Episodic Memory ==========

def record_episode(title: str, outcome: str, lessons: str, context: str = ""):
    """记录经验 (支持版本)"""
    episodic_dir = MEMORY_DIR / CONFIG["episodic"]
    ensure_dir(episodic_dir)
    
    date = datetime.now().strftime('%Y-%m-%d')
    filename = f"{date}-{outcome}.md"
    episode_file = episodic_dir / filename
    
    existing = ""
    if episode_file.exists():
        existing = episode_file.read_text(encoding="utf-8") + "\n\n---\n\n"
    
    content = f"""{existing}## {title}

**时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
**结果**: {outcome}

### 上下文
{context}

### 教训
{lessons}
"""
    episode_file.write_text(content, encoding="utf-8")
    print(f"✅ Episodic: {title} ({outcome})")


def get_episodes(outcome: str = None, limit: int = 5) -> list:
    episodic_dir = MEMORY_DIR / CONFIG["episodic"]
    files = sorted(episodic_dir.glob("*.md"), reverse=True)
    if outcome:
        files = [f for f in files if outcome in f.name]
    return [f.read_text() for f in files[:limit]]


# ========== 自动过期 (来自 agent-memory) ==========

def cleanup_expired():
    """清理过期记忆"""
    today = datetime.now().strftime('%Y-%m-%d')
    cleaned = 0
    
    for category in CONFIG["long"].values():
        dir_path = MEMORY_DIR / category
        if not dir_path.exists():
            continue
        
        for md_file in dir_path.glob("*.md"):
            if md_file.name.startswith("."):
                continue
            
            content = md_file.read_text(encoding="utf-8")
            
            # 检查过期时间
            for line in content.split('\n'):
                if line.startswith("**过期**:"):
                    expire_date = line.split(":")[1].strip()
                    if expire_date and expire_date != "永不过期":
                        if expire_date < today:
                            md_file.unlink()
                            cleaned += 1
                            print(f"🗑️ 删除过期: {md_file.name}")
                    break
    
    if cleaned:
        # 更新索引
        try:
            import subprocess
            subprocess.run(["python3", str(MEMORY_DIR / "index.py")], capture_output=True)
        except:
            pass
    
    return cleaned


def list_memories() -> dict:
    """列出所有记忆 (带统计)"""
    stats = {}
    
    # Preferences
    prefs = len(list((MEMORY_DIR / CONFIG["long"]["preferences"]).glob("*.md")))
    prefs = max(0, prefs - 1)
    stats["preferences"] = prefs
    
    # Entities
    stats["entities"] = len(list((MEMORY_DIR / CONFIG["long"]["entities"]).glob("*.md")))
    
    # Events
    stats["events"] = len(list((MEMORY_DIR / CONFIG["long"]["events"]).glob("*.md")))
    
    # Episodes
    stats["episodes"] = len(list((MEMORY_DIR / CONFIG["episodic"]).glob("*.md")))
    
    # Persona
    stats["persona"] = len(list((MEMORY_DIR / CONFIG["persona"]).glob("*.md")))
    
    return stats


# ========== 工具函数 ==========

def update_abstract(category: str):
    category_dir = MEMORY_DIR / category
    if not category_dir.exists():
        return
    files = [f for f in category_dir.glob("*.md") if not f.name.startswith(".")]
    if not files:
        return
    abstract = [f"- {f.stem}" for f in files]
    abstract_file = category_dir / ".abstract.md"
    content = f"# {Path(category).stem.capitalize()} 摘要\n\n**总数**: {len(files)}\n\n" + "\n".join(abstract)
    abstract_file.write_text(content, encoding="utf-8")


def read_core_memory():
    print("\n" + "=" * 50)
    print("🧠 读取核心记忆")
    print("=" * 50)
    
    working = get_working_memory()
    if working:
        print("\n📋 Working Memory:")
        for k, v in working.items():
            print(f"  {k}: {v.get('value', '')[:50]}")
    
    pref_abstract = MEMORY_DIR / CONFIG["long"]["preferences"] / ".abstract.md"
    if pref_abstract.exists():
        print("\n⭐ Preferences L0:")
        print(pref_abstract.read_text()[:300])
    
    print("\n" + "=" * 50)


def daily_summary():
    print("\n" + "=" * 50)
    print("📊 每日记忆总结")
    print("=" * 50)
    
    stats = list_memories()
    print(f"\nLong Term:")
    print(f"  偏好: {stats['preferences']}")
    print(f"  实体: {stats['entities']}")
    print(f"  事件: {stats['events']}")
    print(f"\nPersona: {stats['persona']}")
    print(f"Episodic: {stats['episodes']}")
    print("\n" + "=" * 50)


def main():
    if len(sys.argv) < 2:
        read_core_memory()
    elif sys.argv[1] == "read":
        read_core_memory()
    elif sys.argv[1] == "summary":
        daily_summary()
    elif sys.argv[1] == "cleanup":
        cleaned = cleanup_expired()
        print(f"🧹 清理了 {cleaned} 个过期记忆")
    elif sys.argv[1] == "stats":
        stats = list_memories()
        print(json.dumps(stats, indent=2))
    elif sys.argv[1] == "working" and len(sys.argv) >= 4:
        set_working_memory(sys.argv[2], sys.argv[3])
    elif sys.argv[1] == "pref" and len(sys.argv) >= 4:
        reason = sys.argv[4] if len(sys.argv) > 4 else ""
        expires = sys.argv[5] if len(sys.argv) > 5 else None
        if expires:
            expires = int(expires)
        record_preference(sys.argv[2], sys.argv[3], reason, expires)
    elif sys.argv[1] == "entity" and len(sys.argv) >= 4:
        entity_type = sys.argv[4] if len(sys.argv) > 4 else "person"
        record_entity(sys.argv[2], sys.argv[3], entity_type)
    elif sys.argv[1] == "event" and len(sys.argv) >= 3:
        desc = sys.argv[2] if len(sys.argv) > 2 else ""
        outcome = sys.argv[3] if len(sys.argv) > 3 else None
        record_event(sys.argv[2], desc, outcome)
    elif sys.argv[1] == "episode" and len(sys.argv) >= 4:
        context = sys.argv[4] if len(sys.argv) > 4 else ""
        record_episode(sys.argv[2], sys.argv[3], sys.argv[4] if len(sys.argv) > 4 else "", context)
    elif sys.argv[1] == "persona" and len(sys.argv) >= 4:
        set_persona(sys.argv[2], sys.argv[3])
    else:
        print("""
🧠 Open Memory System v3.1
基于 OpenViking + agent-memory

使用方式:
  python memory.py                    # 读取核心记忆
  python memory.py read              # 同上
  python memory.py summary          # 每日总结
  python memory.py cleanup          # 清理过期记忆
  python memory.py stats            # 统计
  
  # Working Memory
  python memory.py working <key> <value>
  
  # Long Term Memory
  python memory.py pref <key> <value> [reason] [expires_days]
  python memory.py entity <name> <desc> [type]
  python memory.py event <title> [description] [outcome]
  
  # Persona
  python memory.py persona <key> <value>
  
  # Episodic
  python memory.py episode <title> <outcome> <lessons> [context]
""")


if __name__ == "__main__":
    main()


path: scripts/auto-save-memory/handler.js
content: /**
 * Auto Save Memory Hook
 * Session end时自动保存学习要点到记忆系统
 */
const fs = require('fs');
const path = require('path');

const WORKSPACE = process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspace';
const LEARNINGS_DIR = path.join(WORKSPACE, '.learnings');
const MEMORY_DIR = path.join(WORKSPACE, 'memory');

function getLearnings() {
  if (!fs.existsSync(LEARNINGS_DIR)) return [];
  return fs.readdirSync(LEARNINGS_DIR)
    .filter(f => f.endsWith('.md'))
    .map(f => ({
      name: f,
      path: path.join(LEARNINGS_DIR, f),
      mtime: fs.statSync(path.join(LEARNINGS_DIR, f)).mtime
    }))
    .sort((a, b) => b.mtime - a.mtime);
}

async function saveLearnings() {
  const learnings = getLearnings();
  if (learnings.length === 0) return ['No new learnings'];

  const results = [];
  for (const f of learnings.slice(0, 3)) {
    const content = fs.readFileSync(f.path, 'utf-8');
    const destDir = path.join(MEMORY_DIR, 'user', 'events');
    if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
    const dest = path.join(destDir, `learned-${f.name}`);
    fs.writeFileSync(dest, content);
    results.push(`Saved: ${f.name}`);
  }
  return results;
}

exports.handler = async (event) => {
  try {
    const results = await saveLearnings();
    return { logs: results };
  } catch (err) {
    return { logs: [`Error: ${err.message}`] };
  }
};


path: scripts/auto-save-memory/HOOK.md
content: ---
name: auto-save-memory
description: "会话结束时自动保存学习要点"
metadata:
  {
    "openclaw": {
      "emoji": "💾",
      "events": ["session:end"],
      "requires": {}
    }
  }
---

# Auto Save Memory Hook

会话结束时自动保存 .learnings/ 目录中的学习要点到记忆系统。


path: scripts/auto-load-memory/handler.js
content: /**
 * Auto Load Memory Hook
 * Session start时自动加载核心记忆到上下文
 */
const fs = require('fs').promises;
const path = require('path');

exports.handler = async (event) => {
  const workspaceDir = process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspace';
  const memoryDir = path.join(workspaceDir, 'memory');
  const logs = [];

  try {
    // 1. Read MEMORY.md
    const memoryPath = path.join(workspaceDir, 'MEMORY.md');
    try {
      const memory = await fs.readFile(memoryPath, 'utf-8');
      logs.push(`Loaded MEMORY.md (${memory.length} chars)`);
    } catch {
      logs.push('MEMORY.md not found, skipping');
    }

    // 2. Read today's short-term memory
    const today = new Date().toISOString().split('T')[0];
    const todayPath = path.join(memoryDir, 'short-term', `${today}.md`);
    try {
      const todayLog = await fs.readFile(todayPath, 'utf-8');
      logs.push(`Loaded today's short-term: ${today}.md (${todayLog.length} chars)`);
    } catch {
      logs.push('Today short-term not found, skipping');
    }

    // 3. Read preferences abstract
    const prefAbstract = path.join(memoryDir, 'user', 'preferences', '.abstract.md');
    try {
      const prefs = await fs.readFile(prefAbstract, 'utf-8');
      logs.push(`Loaded preferences abstract (${prefs.length} chars)`);
    } catch {
      logs.push('Preferences abstract not found, skipping');
    }

    return { logs, memory: logs.join(' | ') };
  } catch (err) {
    return { logs: [`Error: ${err.message}`] };
  }
};


path: scripts/auto-load-memory/HOOK.md
content: ---
name: auto-load-memory
description: "会话启动时自动加载核心记忆"
metadata:
  {
    "openclaw": {
      "emoji": "💾",
      "events": ["agent:bootstrap"],
      "requires": { "bins": ["python3"] }
    }
  }
---

# Auto Load Memory Hook

会话开始时自动加载三层记忆。

## 功能
1. 监听 `agent:bootstrap` 事件
2. 运行 `memory/memory.py read` 获取记忆内容
3. 将记忆内容注入 Agent 上下文

## 加载的记忆内容
- MEMORY.md 长期记忆
- 今日 short-term 日志
- preferences 摘要


path: crons/memory-crons.txt
content: # Memory System Cron 任务配置

## 推荐 Cron 任务

### 1. 每日记忆清理 (每天 00:00)
清理过期记忆，自动整理索引。

```bash
openclaw cron add \
  --name daily-memory-cleanup \
  --schedule "0 0 * * *" \
  --agent main \
  --execute "python3 /path/to/scripts/memory.py cleanup && python3 /path/to/scripts/memory.py summary"
```

### 2. 每日 L2 提炼 (每天 21:00)
将 short-term 记忆中有价值的内容提炼到 long-term events。

```bash
openclaw cron add \
  --name daily-memory-distill \
  --schedule "0 21 * * *" \
  --agent main \
  --message "请执行: MEMORY_DIR=~/.openclaw/workspace/memory python3 /root/.openclaw/workspace/skills/open-memory-system/scripts/distill_l2.py"
```

### 3. 每周记忆蒸馏 (每周一 06:00)
将短记忆提炼到长期记忆。

```bash
openclaw cron add \
  --name weekly-memory-distill \
  --schedule "0 6 * * 1" \
  --agent main \
  --execute "python3 /path/to/scripts/memory.py distill"
```

## 快速安装

### Step 1: 创建 cron（手动模式）
```bash
# 每日清理
openclaw cron add \
  --name daily-memory-cleanup \
  --schedule "0 0 * * *" \
  --agent main \
  --message "请执行: MEMORY_DIR=~/.openclaw/workspace/memory python3 /root/.openclaw/workspace/skills/open-memory-system/scripts/memory.py cleanup && python3 /root/.openclaw/workspace/skills/open-memory-system/scripts/memory.py summary"
```

### Step 2: 验证安装
```bash
openclaw cron list | grep memory
```

### Step 3: 测试运行
```bash
MEMORY_DIR=~/.openclaw/workspace/memory python3 /root/.openclaw/workspace/skills/open-memory-system/scripts/memory.py summary
```


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