Skill: canmaxfire/bounded-memory (version 1.1.1)

--- name: bounded-memory description: Gives your OpenClaw AI a perfect memory. Ask things like "did we discuss this before?", "what did we decide about X?", and "find that conversation about Y" — it searches through all your past conversations instantly. Great for recalling decisions, preferences, and context from months ago. Use when: (1) user asks "did we talk about X before?", (2) "search my old conversations", (3) "find what we decided about project Y", (4) "remember what I asked last week". Triggers: "search sessions", "find earlier conversation", "recall past discussion", "what did I say about". Privacy: search indexing and execution are 100% offline. LLM summarization is opt-in (use --llm flag) and disabled by default. No external API calls without your explicit consent. --- # Bounded Memory Gives your OpenClaw AI agent a **perfect memory** — it can recall anything you've ever discussed, even from months ago. ## What It Does Without this skill: each OpenClaw session starts fresh. The AI forgets everything. With this skill: ask things like: - "Did we discuss X before?" - "What did we decide about Y?" - "Find that conversation from last month" ## Privacy Design | What runs | How | |-----------|-----| | Search indexing | ✅ 100% offline — SQLite only | | Search execution | ✅ 100% offline — no network calls | | LLM summarization | ⚠️ Opt-in only — use `--llm` flag to enable | **No external API calls by default.** The `--llm` flag (disabled by default) sends snippets to your configured LLM for summarization — only when you explicitly ask for it. ## Quick Start ```bash # Index your conversations (first time) python3 skills/session-search/scripts/index-sessions.py --agent main # Search (fully offline) python3 skills/session-search/scripts/search-sessions.py "what did we decide about the logo design" # Search with AI summary (opt-in) python3 skills/session-search/scripts/search-sessions.py "question" --llm ``` ## What It Solves | Problem | Without | With Bounded Memory | |---------|---------|---------------------| | "I asked this before but can't remember" | AI has no idea | Instant recall | | "What did we decide in that meeting?" | Forgot | Searches all sessions | | "Did I mention this before?" | No way to know | Searches everything | ## Example ``` You: "Search our conversations about the robot project" → Found 3 discussions: 1. [Last week] We discussed the design direction... 2. [2 weeks ago] You asked about pricing... 3. [Last month] The AI suggested adding... ```

path: README.md
content: # Bounded Memory

[![Version](https://img.shields.io/badge/version-v1.1.1-blue.svg)](https://github.com/canmaxice-maker/bounded-memory)
[![ClawHub](https://img.shields.io/badge/ClawHub-bounded--memory-green.svg)](https://clawhub.com/bounded-memory)

**Gives your OpenClaw AI a perfect memory.** Search through all your past conversations instantly — recall decisions, preferences, and context from months ago.

## The Problem It Solves

OpenClaw AI starts fresh every session. It forgets everything from previous chats.

That means every new session, you have to re-explain:
- Your projects and preferences
- Past decisions you've made
- What you discussed and agreed on
- Context from earlier conversations

**Bounded Memory fixes this.** It gives your AI an always-on memory of everything you've ever discussed.

## What You Can Do

```
"Did we discuss the logo design?"
→ Instantly finds all relevant past conversations

"What did we decide about the budget?"
→ Shows you the exact discussion and decision

"Find that conversation about the robot project from last month"
→ Pulls it up immediately
```

## How It Works

```
1. Index (once)
   Your conversation files → Local search database (SQLite)

2. Search (anytime you ask)
   Your question → Instant results from full history

3. Recall
   You get the answer + the original conversation context
```

## Privacy First

- **100% offline search** — SQLite FTS5 runs entirely on your machine
- **No external services** — search indexing never calls any API
- **LLM is opt-in** — disabled by default, only enabled when you add `--llm`
- **You control it** — uninstall anytime, remove the database anytime
- **Git-ignored** — your conversation database is never committed

## Installation

```bash
# ClawHub (recommended)
clawhub install bounded-memory

# Manual
git clone https://github.com/canmaxfire/bounded-memory.git
mv bounded-memory ~/.openclaw/workspace/main/skills/session-search
```

## Quick Start

```bash
# 1. Index your conversations (first time)
python3 ~/.openclaw/workspace/main/skills/session-search/scripts/index-sessions.py --agent main

# 2. Search (fully offline, no external calls)
python3 ~/.openclaw/workspace/main/skills/session-search/scripts/search-sessions.py "your question"

# 3. Optional: enable AI summaries of results (requires API key)
python3 ~/.openclaw/workspace/main/skills/session-search/scripts/search-sessions.py "question" --llm
```

## Privacy Details

| Feature | Behavior |
|---------|----------|
| Search indexing | ✅ Fully offline — SQLite FTS5 only |
| Search execution | ✅ Fully offline — no network calls |
| LLM summarization | ⚠️ Opt-in only — disabled by default |
| API key | Only read if `--llm` flag is used |

The `--llm` flag reads your LLM API key from `~/.openclaw/openclaw.json` and sends the conversation snippets to your configured LLM endpoint for summarization. Without `--llm`, no external calls are made.

## What Changes

| Before | After |
|--------|-------|
| "I know we discussed this before but..." | "Found it — we talked about this on April 3rd" |
| AI has no idea what you asked last month | Instantly recalls months of conversations |
| Re-explaining context every session | Context carries across all sessions |
| Forgetting important decisions | Never lose track of what was decided |

## Architecture

```
~/.openclaw/agents/main/sessions/*.jsonl
    ↓ (indexed once, updated daily)
~/.openclaw/workspace/main/skills/session-search/db/sessions.db
    ↓ (searched on demand — no network)
Instant results from your full conversation history
```

## Credits

Inspired by [Hermes Agent](https://github.com/NousResearch/hermes-agent)'s bounded memory architecture.

## Changelog

| Version | Date | Changes |
|---------|------|---------|
| [v1.1.1](https://github.com/canmaxice-maker/bounded-memory/releases/tag/v1.1.1) | 2026-04-21 | Fix: LLM is now truly opt-in, docs match code |
| [v1.1.0](https://github.com/canmaxice-maker/bounded-memory/releases/tag/v1.1.0) | 2026-04-21 | Rewrite: plain language, user benefit focus |
| [v1.0.0](https://github.com/canmaxice-maker/bounded-memory/releases/tag/v1.0.0) | 2026-04-21 | Initial release |

## License

MIT


path: scripts/index-sessions.py
content: #!/usr/bin/env python3
"""
Session Indexer — scan OpenClaw session .jsonl files and index into SQLite FTS5.
Usage: python index-sessions.py [--agent main] [--incremental] [--all-agents]
"""
import sqlite3
import json
import glob
import os
import sys
import argparse
from datetime import datetime

# Configurable paths via env vars
AGENTS_DIR = os.environ.get("OPENCLAW_AGENTS_DIR", os.path.expanduser("~/.openclaw/agents"))
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DB_DIR = os.environ.get("SESSION_SEARCH_DB_DIR", os.path.join(SKILL_DIR, "db"))
DB_PATH = os.path.join(DB_DIR, "sessions.db")

def get_session_files(agent_id="main"):
    """Get all .jsonl session files for an agent."""
    session_dir = os.path.join(AGENTS_DIR, agent_id, "sessions")
    if not os.path.isdir(session_dir):
        return []
    return sorted(glob.glob(f"{session_dir}/*.jsonl"), key=os.path.getmtime, reverse=True)

def get_all_agent_ids():
    """Discover all agent IDs from agents directory."""
    if not os.path.isdir(AGENTS_DIR):
        return []
    return [d for d in os.listdir(AGENTS_DIR)
            if os.path.isdir(os.path.join(AGENTS_DIR, d, "sessions"))]

def extract_messages(jsonl_path):
    """Extract user/assistant messages from a session file."""
    messages = []
    try:
        with open(jsonl_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    d = json.loads(line)
                except json.JSONDecodeError:
                    continue

                if d.get("type") != "message":
                    continue

                msg = d.get("message", {})
                role = msg.get("role", "?")
                if role not in ("user", "assistant"):
                    continue

                content = msg.get("content", [])
                texts = []
                for block in content:
                    if isinstance(block, dict):
                        if block.get("type") == "text":
                            t = block.get("text", "").strip()
                            if t:
                                texts.append(t)
                        elif block.get("type") == "toolCall":
                            tc = block.get("name", "?")
                            args = block.get("arguments", {})
                            if isinstance(args, dict):
                                cmd = args.get("command", "")[:100]
                                texts.append(f"[tool: {tc}] {cmd}")
                            else:
                                texts.append(f"[tool: {tc}]")

                full_text = "\n".join(texts)
                if not full_text:
                    continue

                messages.append({
                    "message_id": d.get("id", ""),
                    "role": role,
                    "text": full_text,
                    "timestamp": msg.get("timestamp", d.get("timestamp", "")),
                })
    except Exception as e:
        print(f"  ⚠️  Failed to read {jsonl_path}: {e}", file=sys.stderr)
    return messages

def init_db():
    """Create SQLite FTS5 tables."""
    os.makedirs(DB_DIR, exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("""
        CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
            session_id, message_id, role, text, timestamp,
            tokenize='unicode61 remove_diacritics 2'
        )
    """)
    c.execute("""
        CREATE TABLE IF NOT EXISTS index_meta (
            session_file TEXT PRIMARY KEY,
            last_modified TEXT,
            entries_indexed INTEGER DEFAULT 0
        )
    """)
    conn.commit()
    return conn

def index_agent(agent_id, conn, incremental=False):
    """Index sessions for one agent."""
    session_files = get_session_files(agent_id)
    if not session_files:
        return 0, 0

    total_indexed = 0
    files_processed = 0

    for sf in session_files:
        sf_abs = os.path.abspath(sf)
        mtime = datetime.fromtimestamp(os.path.getmtime(sf_abs)).isoformat()

        if incremental:
            c = conn.cursor()
            c.execute("SELECT last_modified FROM index_meta WHERE session_file = ?", (sf_abs,))
            row = c.fetchone()
            if row and row[0] == mtime:
                continue

        messages = extract_messages(sf_abs)
        if not messages:
            continue

        session_id = os.path.basename(sf_abs).replace(".jsonl", "")

        if incremental:
            conn.execute("DELETE FROM sessions_fts WHERE session_id = ?", (session_id,))

        entries = [
            (session_id, m["message_id"], m["role"], m["text"], m["timestamp"])
            for m in messages
        ]
        conn.executemany(
            "INSERT OR REPLACE INTO sessions_fts VALUES (?, ?, ?, ?, ?)",
            entries
        )
        conn.execute(
            "INSERT OR REPLACE INTO index_meta VALUES (?, ?, ?)",
            (sf_abs, mtime, len(messages))
        )

        total_indexed += len(messages)
        files_processed += 1

    return total_indexed, files_processed

def main():
    parser = argparse.ArgumentParser(description="Index OpenClaw session files into FTS5")
    parser.add_argument("--agent", default="main", help="Agent ID (default: main)")
    parser.add_argument("--all-agents", action="store_true", help="Index all agents found in agents dir")
    parser.add_argument("--incremental", action="store_true", help="Skip files not modified since last index")
    args = parser.parse_args()

    conn = init_db()
    grand_total = 0
    grand_files = 0

    if args.all_agents:
        for agent_id in get_all_agent_ids():
            print(f"\n📁 Indexing agent: {agent_id}")
            n, f = index_agent(agent_id, conn, incremental=args.incremental)
            print(f"   {'+' if n else '  '}{f} files, {n} messages")
            grand_total += n
            grand_files += f
    else:
        print(f"📁 Indexing agent: {args.agent}")
        grand_total, grand_files = index_agent(args.agent, conn, incremental=args.incremental)
        print(f"   {'+' if grand_total else '  '}{grand_files} files, {grand_total} messages")

    conn.commit()
    conn.close()

    print(f"\n✅ Done — {grand_files} files, {grand_total} messages indexed")
    print(f"   DB: {DB_PATH}")

if __name__ == "__main__":
    main()


path: scripts/search-sessions.py
content: #!/usr/bin/env python3
"""
Session Search — FTS5 full-text search over OpenClaw session histories.
Usage: python search-sessions.py "query" [--limit 5] [--llm] [--all-agents]

Search is fully offline. LLM summarization is opt-in (use --llm to enable).
"""
import sqlite3
import os
import sys
import re
import argparse
import json
import urllib.request
import urllib.error

SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DB_DIR = os.environ.get("SESSION_SEARCH_DB_DIR", os.path.join(SKILL_DIR, "db"))
DB_PATH = os.path.join(DB_DIR, "sessions.db")

def get_api_key():
    """Find MiniMax/OpenAI API key from environment or OpenClaw config."""
    # Try OpenClaw config
    try:
        cfg_path = os.path.expanduser("~/.openclaw/openclaw.json")
        with open(cfg_path) as f:
            config = json.load(f)
        # Check minimax provider in config
        providers = config.get("providers", {})
        for p_name, p_cfg in providers.items():
            for key_name in ("apiKey", "api_key"):
                if key_name in p_cfg:
                    return p_cfg[key_name], p_name
    except Exception:
        pass

    # Env vars
    for var in ("MINIMAX_API_KEY", "OPENAI_API_KEY"):
        key = os.environ.get(var)
        if key:
            return key, var

    return None, None

def fts_search(query, limit=5, all_agents=False):
    """Run FTS5 search, fall back to LIKE on FTS5 errors."""
    if not os.path.exists(DB_PATH):
        print("❌ Database not found. Run index-sessions.py first.", file=sys.stderr)
        return []

    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    c = conn.cursor()

    # Process query for FTS5: handle hyphenated words (memory-core → memory* OR core*)
    processed_words = []
    for word in query.split():
        if re.match(r'^[\w-]+$', word) and '-' in word:
            parts = word.split('-')
            processed_words.append(' OR '.join(f'{p}*' for p in parts))
        elif re.match(r'^[\w*]+$', word):
            processed_words.append(f'"{word}"' if '*' not in word else word)
        else:
            processed_words.append(f'"{word}"')
    fts_query = ' '.join(processed_words)

    try:
        c.execute(f"""
            SELECT session_id, message_id, role, text, timestamp,
                   bm25(sessions_fts) as rank
            FROM sessions_fts
            WHERE sessions_fts MATCH ?
            ORDER BY rank
            LIMIT ?
        """, (fts_query, limit))
        rows = c.fetchall()
    except sqlite3.OperationalError:
        like_q = f"%{query}%"
        c.execute("""
            SELECT session_id, message_id, role, text, timestamp, 0 as rank
            FROM sessions_fts
            WHERE text LIKE ?
            ORDER BY timestamp DESC
            LIMIT ?
        """, (like_q, limit))
        rows = c.fetchall()

    conn.close()
    return [dict(r) for r in rows]

def format_results(results):
    """Format search results for display."""
    if not results:
        return "No matching sessions found."

    lines = []
    for i, r in enumerate(results, 1):
        ts = str(r.get("timestamp", ""))[:10] if r.get("timestamp") else "?"
        role_label = "👤 user" if r.get("role") == "user" else "🤖 assistant"
        text = r.get("text", "")[:400]
        if len(r.get("text", "")) > 400:
            text += "..."
        lines.append(f"{i}. [{ts}] {role_label}\n   {text}\n")
    return "\n".join(lines)

def summarize(query, results_text):
    """Summarize search results using MiniMax LLM."""
    api_key, provider = get_api_key()
    if not [REDACTED_SECRET] None, "⚠️  No API key found. Skipping summary."

    # MiniMax compatible OpenAI endpoint
    BASE_URL = "https://api.minimax.chat/v1"
    MODEL = "MiniMax-M2.7"

    prompt = f"""User asked: "{query}"

Relevant conversation excerpts:
---
{results_text}
---

Briefly summarize whether these excerpts answer the user's question. Answer in Chinese, 1-2 sentences."""

    payload = {
        "model": MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": 300,
        "temperature": 0.3,
    }

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}",
    }

    req = urllib.request.Request(
        f"{BASE_URL}/chat/completions",
        data=json.dumps(payload).encode("utf-8"),
        headers=headers,
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
            return content, None
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8")[:200]
        return None, f"API error {e.code}: {body}"
    except Exception as e:
        return None, f"Request failed: {e}"

def main():
    parser = argparse.ArgumentParser(description="Search session histories")
    parser.add_argument("query", help="Search query")
    parser.add_argument("--limit", type=int, default=5, help="Result count (default: 5)")
    parser.add_argument("--llm", action="store_true", help="Enable LLM summary (requires API key in ~/.openclaw/openclaw.json)")
    args = parser.parse_args()

    results = fts_search(args.query, limit=args.limit)
    if not results:
        print("🔍 No matching sessions found.")
        sys.exit(0)

    formatted = format_results(results)
    print(f"🔍 {len(results)} results:\n\n{formatted}")

    if args.llm:
        summary, err = summarize(args.query, formatted)
        if err:
            print(f"\n{err}")
        elif summary:
            print(f"\n💡 {summary}")

if __name__ == "__main__":
    main()


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