Back to skills
SkillHub ClubShip Full StackFull Stack

session-history

Search and browse past conversation history across all sessions. Use when recalling prior work, finding old discussions, resuming dropped threads, or when the user references something from a previous conversation that isn't in memory files. Also use when asked to "remember" something discussed before, find "that conversation about X", or continue work from a past session.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,130
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
A88.4

Install command

npx @skill-hub/cli install openclaw-skills-session-history

Repository

openclaw/skills

Skill path: skills/amor71/session-history

Search and browse past conversation history across all sessions. Use when recalling prior work, finding old discussions, resuming dropped threads, or when the user references something from a previous conversation that isn't in memory files. Also use when asked to "remember" something discussed before, find "that conversation about X", or continue work from a past session.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install session-history into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding session-history to shared team environments
  • Use session-history for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: session-history
description: Search and browse past conversation history across all sessions. Use when recalling prior work, finding old discussions, resuming dropped threads, or when the user references something from a previous conversation that isn't in memory files. Also use when asked to "remember" something discussed before, find "that conversation about X", or continue work from a past session.
---

# Session History

Search through past OpenClaw session transcripts (JSONL files in `~/.openclaw/agents/*/sessions/`).

## Quick Reference

```bash
# Search for conversations about a topic
python3 scripts/search_sessions.py "gclid pipeline error"

# List recent sessions
python3 scripts/search_sessions.py --list --days 3

# Search specific agent's history
python3 scripts/search_sessions.py "flight monitor" --agent main

# Wider time range
python3 scripts/search_sessions.py "quantum encryption" --days 30 --max-results 5
```

## Workflow

1. Run `search_sessions.py` with the user's query terms to find relevant sessions
2. Use `sessions_history` tool with the `sessionKey` to pull full context from a match
3. If `sessions_history` doesn't work (old/closed sessions), read the JSONL file directly with `read`
4. Summarize what was found β€” don't dump raw transcripts

## When to Use

- User says "remember when we discussed X?" or "we talked about Y last week"
- Resuming a thread that isn't captured in memory files
- Finding a decision, code snippet, or error from a past session
- Cross-referencing what was said vs what's in MEMORY.md

## Tips

- Also check `memory_search` first β€” it indexes session transcripts too
- Combine both: `memory_search` for semantic matching, `search_sessions.py` for keyword/exact matching
- The script searches user AND assistant messages
- JSONL path format: `~/.openclaw/agents/{agent_id}/sessions/{session_uuid}.jsonl`


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/search_sessions.py

```python
#!/usr/bin/env python3
"""Search through OpenClaw session transcripts for relevant conversations.

Usage:
    python3 search_sessions.py <query> [--days N] [--max-results N] [--agent AGENT]
    python3 search_sessions.py --list [--days N] [--agent AGENT]

Examples:
    python3 search_sessions.py "error blog patches"
    python3 search_sessions.py "flight monitor" --days 7
    python3 search_sessions.py --list --days 3
    python3 search_sessions.py "gclid pipeline" --agent main --max-results 5
"""

import argparse
import json
import os
import re
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path


def find_session_dirs():
    """Find all agent session directories."""
    base = Path.home() / ".openclaw" / "agents"
    dirs = {}
    if base.exists():
        for agent_dir in base.iterdir():
            sessions_dir = agent_dir / "sessions"
            if sessions_dir.is_dir():
                dirs[agent_dir.name] = sessions_dir
    return dirs


def parse_session(path: Path):
    """Parse a JSONL session file into metadata + messages."""
    meta = {}
    messages = []
    try:
        with open(path) as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                obj = json.loads(line)
                t = obj.get("type")
                if t == "session":
                    meta = obj
                elif t == "message":
                    msg = obj.get("message", {})
                    role = msg.get("role", "")
                    content = msg.get("content", "")
                    # Extract text from content array
                    if isinstance(content, list):
                        texts = []
                        for block in content:
                            if isinstance(block, dict) and block.get("type") == "text":
                                texts.append(block.get("text", ""))
                        text = "\n".join(texts)
                    elif isinstance(content, str):
                        text = content
                    else:
                        text = ""
                    if text.strip():
                        messages.append({
                            "role": role,
                            "text": text,
                            "timestamp": obj.get("timestamp", ""),
                        })
    except (json.JSONDecodeError, OSError):
        pass
    return meta, messages


def session_timestamp(meta):
    """Get session start time as datetime."""
    ts = meta.get("timestamp", "")
    if ts:
        try:
            return datetime.fromisoformat(ts.replace("Z", "+00:00"))
        except ValueError:
            pass
    return None


def search_text(query_terms, text):
    """Score text against query terms. Returns (score, matched_terms)."""
    text_lower = text.lower()
    matched = []
    score = 0
    for term in query_terms:
        count = text_lower.count(term.lower())
        if count > 0:
            matched.append(term)
            score += count
    return score, matched


def excerpt_around_match(text, term, context_chars=150):
    """Get an excerpt around the first match of a term."""
    idx = text.lower().find(term.lower())
    if idx == -1:
        return text[:300]
    start = max(0, idx - context_chars)
    end = min(len(text), idx + len(term) + context_chars)
    excerpt = text[start:end]
    if start > 0:
        excerpt = "..." + excerpt
    if end < len(text):
        excerpt = excerpt + "..."
    return excerpt


def main():
    parser = argparse.ArgumentParser(description="Search OpenClaw session history")
    parser.add_argument("query", nargs="*", help="Search terms")
    parser.add_argument("--list", action="store_true", help="List recent sessions")
    parser.add_argument("--days", type=int, default=14, help="Look back N days (default: 14)")
    parser.add_argument("--max-results", type=int, default=10, help="Max results (default: 10)")
    parser.add_argument("--agent", default=None, help="Filter by agent id (main, chai, nori)")
    parser.add_argument("--verbose", "-v", action="store_true", help="Show message excerpts")
    args = parser.parse_args()

    if not args.list and not args.query:
        parser.print_help()
        sys.exit(1)

    session_dirs = find_session_dirs()
    if not session_dirs:
        print("No session directories found.")
        sys.exit(1)

    cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
    query_str = " ".join(args.query) if args.query else ""
    query_terms = query_str.split() if query_str else []

    results = []

    agents = [args.agent] if args.agent else list(session_dirs.keys())

    for agent_id in agents:
        if agent_id not in session_dirs:
            continue
        sessions_dir = session_dirs[agent_id]
        for jsonl_path in sessions_dir.glob("*.jsonl"):
            # Quick file age check
            stat = jsonl_path.stat()
            file_mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
            if file_mtime < cutoff:
                continue

            meta, messages = parse_session(jsonl_path)
            ts = session_timestamp(meta)
            if ts and ts < cutoff:
                continue

            if args.list:
                # List mode β€” show session summaries
                msg_count = len(messages)
                user_msgs = [m for m in messages if m["role"] == "user"]
                first_user = user_msgs[0]["text"][:120] if user_msgs else "(no user messages)"
                results.append({
                    "agent": agent_id,
                    "session_id": meta.get("id", jsonl_path.stem),
                    "timestamp": ts.isoformat() if ts else "unknown",
                    "messages": msg_count,
                    "preview": first_user.replace("\n", " "),
                    "path": str(jsonl_path),
                })
            else:
                # Search mode
                all_text = "\n".join(m["text"] for m in messages)
                score, matched = search_text(query_terms, all_text)
                if score > 0:
                    # Find best excerpt
                    best_excerpt = ""
                    for term in matched:
                        for m in messages:
                            if term.lower() in m["text"].lower():
                                best_excerpt = excerpt_around_match(m["text"], term)
                                break
                        if best_excerpt:
                            break

                    user_msgs = [m for m in messages if m["role"] == "user"]
                    first_user = user_msgs[0]["text"][:120] if user_msgs else ""

                    results.append({
                        "agent": agent_id,
                        "session_id": meta.get("id", jsonl_path.stem),
                        "timestamp": ts.isoformat() if ts else "unknown",
                        "score": score,
                        "matched_terms": matched,
                        "messages": len(messages),
                        "preview": first_user.replace("\n", " "),
                        "excerpt": best_excerpt.replace("\n", " "),
                        "path": str(jsonl_path),
                    })

    # Sort
    if args.list:
        results.sort(key=lambda r: r["timestamp"], reverse=True)
    else:
        results.sort(key=lambda r: r["score"], reverse=True)

    results = results[:args.max_results]

    if not results:
        if args.list:
            print(f"No sessions found in the last {args.days} days.")
        else:
            print(f"No sessions matching '{query_str}' in the last {args.days} days.")
        sys.exit(0)

    # Output
    if args.list:
        print(f"πŸ“‹ Recent sessions (last {args.days} days):\n")
        for r in results:
            ts_short = r["timestamp"][:16].replace("T", " ") if r["timestamp"] != "unknown" else "?"
            print(f"  [{r['agent']}] {ts_short} ({r['messages']} msgs) β€” {r['preview'][:80]}")
            if args.verbose:
                print(f"    Path: {r['path']}")
            print()
    else:
        print(f"πŸ” Found {len(results)} sessions matching '{query_str}':\n")
        for i, r in enumerate(results, 1):
            ts_short = r["timestamp"][:16].replace("T", " ") if r["timestamp"] != "unknown" else "?"
            print(f"  {i}. [{r['agent']}] {ts_short} (score: {r['score']}, {r['messages']} msgs)")
            print(f"     Matched: {', '.join(r['matched_terms'])}")
            if r.get("excerpt"):
                print(f"     Excerpt: {r['excerpt'][:200]}")
            print(f"     Path: {r['path']}")
            print()


if __name__ == "__main__":
    main()

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "amor71",
  "slug": "session-history",
  "displayName": "Session History",
  "latest": {
    "version": "1.0.0",
    "publishedAt": 1771532008087,
    "commit": "https://github.com/openclaw/skills/commit/92a09936032ea9e0af0545060d29eb16844271be"
  },
  "history": []
}

```