Skill: jared-goering/ultramemory (version 0.2.4)

--- name: ultramemory description: Structured AI agent memory with temporal versioning, relational tracking, and semantic search. Use when storing facts, recalling context, searching past conversations, tracking how knowledge changed over time, or building entity profiles. Replaces flat MEMORY.md with atomic fact extraction, update/contradict/extend relations, and hybrid semantic+temporal search. Use for any "remember this", "what do I know about X", "when did this change", or cross-session knowledge retrieval. requirements: python: ">=3.10" packages: - ultramemory env: - name: ANTHROPIC_API_KEY required: true description: "Required for LLM fact extraction during ingest. Alternatively set OPENAI_API_KEY." - name: ULTRAMEMORY_DB required: false description: "Path to SQLite database file. Defaults to ./memory.db" --- # Ultramemory Structured agent memory: extracts atomic facts from text, detects relations to existing knowledge (updates, contradicts, extends), embeds for semantic search, and auto-builds entity profiles. PyPI: `ultramemory` | GitHub: `jared-goering/ultramemory` ## Setup ```bash # Install (creates venv automatically on first run) pip install ultramemory # Or from source git clone https://github.com/jared-goering/ultramemory.git cd ultramemory && pip install -e . ``` **Requirements:** - Python 3.10+ - An LLM API key for fact extraction (Anthropic recommended, OpenAI also works) - Local embeddings load automatically (sentence-transformers/all-MiniLM-L6-v2, ~80MB first run) **Environment:** ```bash export ANTHROPIC_API_KEY="sk-ant-..." # or OPENAI_API_KEY export ULTRAMEMORY_DB="./memory.db" # default: memory.db in current dir ``` ## Quick Start ```python from ultramemory import MemoryEngine engine = MemoryEngine(db_path="memory.db") # Ingest text (extracts atomic facts, detects relations, builds profiles) results = engine.ingest("Jared moved from Bel Aire to Wichita. The baby is due in July.") # Search matches = engine.search("Where does Jared live?", top_k=5) # Recall (compact context block for agent prompts) context = engine.recall("current projects and priorities", top_k=5) ``` ## CLI Usage The `scripts/memory.sh` wrapper handles venv activation and API key loading. ### Store memories (ingest) ```bash bash scripts/memory.sh ingest \ "Jared moved to Wichita. The baby is now due in August, not July." \ --session "main-2026-03-22" --agent kit ``` Categories: `person`, `preference`, `project`, `decision`, `event`, `insight` Relations auto-detected: `updates` (supersedes old fact), `contradicts`, `extends`, `supports`, `derives` When a memory `updates` an existing one, the old memory is marked `superseded` and the new one gets an incremented version. ### Recall (agent-optimized search) Compact context block for injecting into agent prompts: ```bash bash scripts/memory.sh recall "Where does Jared live?" --top-k 5 ``` Output: ``` [person] Jared moved to Wichita. (v2, current, 89% match) -> updates: Jared lives in Bel Aire, KS. [project] Jared teaches Human-Centric Design at WSU. (v1, current, 72% match) ``` ### Search (full JSON) ```bash bash scripts/memory.sh search "baby due date" --top-k 10 # Include superseded memories: bash scripts/memory.sh search "baby due date" --all # Time travel: what did we know on March 1? bash scripts/memory.sh search "baby due date" --as-of 2026-03-01 ``` ### Entity operations ```bash bash scripts/memory.sh entities # List all known entities bash scripts/memory.sh history Jared # Version timeline bash scripts/memory.sh profile Jared # Auto-built profile bash scripts/memory.sh stats # Counts, categories ``` ## Integration Patterns ### Session startup At the start of any session, hydrate context: ```bash bash scripts/startup-recall.sh <agent-id> ``` ### Post-conversation ingest After meaningful conversations, pass the text directly: ```bash bash scripts/memory.sh ingest "User decided to use React for the frontend. Budget is $50k." --session $SESSION_KEY --agent $AGENT_ID ``` ### API Server For multi-agent setups, run the API server (requires separate install from PyPI): ```bash pip install ultramemory python3 -m uvicorn ultramemory.server:app --port 8642 --host 127.0.0.1 ``` Endpoints: `POST /api/ingest`, `POST /api/search`, `POST /api/recall`, `GET /api/stats` ### Advanced: Auto-ingest from session transcripts For continuous ingestion from session files, see the [GitHub repo](https://github.com/jared-goering/ultramemory) which includes `auto_ingest.py` and `live_ingest.sh` scripts. ## Why Not Just Use MEMORY.md? You'll outgrow a flat file fast. But you also can't replace it entirely. We tried. At 18,000+ memories, search results get noisy. The DB is great at answering "what happened Tuesday?" but terrible as a session primer. Meanwhile, MEMORY.md is perfect for "who am I, who's my human, what are we working on" but can't hold 18K facts in 2K tokens. The architecture we landed on uses three layers: **Layer 1: MEMORY.md (always loaded, zero cost)** Curated essentials under 2K tokens. Loaded every session, no API calls, no latency. Contains identity, active projects, key preferences. Think of it as working memory. **Layer 2: Ultramemory plugin (opportunistic injection)** When a message arrives, the plugin searches the DB and injects relevant memories if they score above a similarity threshold (we use 0.55). The agent never explicitly asks for this. It just gets richer context when the DB has something relevant. **Layer 3: Ultramemory direct (precision recall)** The agent explicitly searches when it needs specifics. "What was the benchmark result?" or "When did we decide to drop NYC?" This is the full 18K+ memory DB with semantic search, temporal filtering, and entity profiles. MEMORY.md is the backup and the bootstrap. Ultramemory is the brain. You need both. ## Architecture - **Storage:** Single SQLite DB with WAL mode. Agent-ID tagging for multi-agent isolation. - **Extraction:** LLM extracts atomic facts, categorizes, detects entities, finds relations to existing memories. - **Embeddings:** Local sentence-transformers (384-dim). No API calls for search. - **Relations:** UPDATE, EXTEND, CONTRADICT, SUPPORT, DERIVE. Version chain with superseded tracking. - **Profiles:** Auto-built entity profiles from accumulated facts. - **Events:** Structured event extraction with canonical clustering and dedup. - **Temporal:** Deterministic temporal expression parsing and date arithmetic (no LLM needed). ## Cost - Ingest: ~$0.01-0.02 per call (3 LLM calls: extract, relate, profile) - Search/recall: Free (local embeddings + SQLite) - Embedding model: ~80MB download on first run, then cached ## Benchmark 80% accuracy on LongMemEval_s (production-relevant questions). 32ms median search latency.

path: scripts/memory.py
content: #!/usr/bin/env python3
"""CLI wrapper for the OpenClaw Memory Engine.

Usage:
  memory.py ingest "Some text about facts" [--session KEY] [--agent ID] [--date YYYY-MM-DD]
  memory.py search "query" [--top-k 10] [--all] [--as-of YYYY-MM-DD]
  memory.py recall "query" [--top-k 5]          # Agent-optimized: returns compact context block
  memory.py graph                                 # Dump full graph as JSON
  memory.py stats                                 # Memory counts
  memory.py history ENTITY_NAME                   # Version timeline for entity
  memory.py profile ENTITY_NAME                   # Auto-built entity profile
  memory.py entities                              # List all known entities
"""

import argparse
import json
import os
import sys

# ultramemory must be pip-installed: pip install ultramemory
DB_PATH = os.environ.get("ULTRAMEMORY_DB", os.environ.get("MEMORY_DB", "memory.db"))


def get_engine():
    from ultramemory.engine import MemoryEngine
    return MemoryEngine(db_path=DB_PATH)


def cmd_ingest(args):
    engine = get_engine()
    memories = engine.ingest(
        args.text,
        session_key=args.session or "cli",
        agent_id=args.agent or "kit",
        document_date=args.date,
    )
    print(json.dumps(memories, indent=2))
    print(f"\n✓ Extracted {len(memories)} memories", file=sys.stderr)


def cmd_search(args):
    engine = get_engine()
    results = engine.search(
        args.query,
        top_k=args.top_k,
        current_only=not args.all,
        as_of_date=args.as_of,
    )
    print(json.dumps(results, indent=2))


def cmd_recall(args):
    """Agent-optimized search: returns a compact context block ready for injection."""
    engine = get_engine()
    results = engine.search(
        args.query,
        top_k=args.top_k,
        current_only=True,
    )
    if not results:
        print("No relevant memories found.")
        return

    lines = []
    for r in results:
        sim_pct = int(r["similarity"] * 100)
        status = "current" if r["is_current"] else "superseded"
        line = f"[{r['category']}] {r['content']} (v{r['version']}, {status}, {sim_pct}% match)"
        
        # Include relations inline if present
        if r.get("relations"):
            for rel in r["relations"]:
                line += f"\n  → {rel['relation']}: {rel['related_content']}"
        
        lines.append(line)

    print("\n".join(lines))


def cmd_graph(args):
    import sqlite3
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row

    nodes = conn.execute(
        """SELECT id, content, category, confidence, document_date, event_date,
                  is_current, version, source_session, source_agent, created_at
           FROM memories ORDER BY created_at"""
    ).fetchall()

    edges = conn.execute(
        """SELECT mr.from_memory as source, mr.to_memory as target,
                  mr.relation as type, mr.context
           FROM memory_relations mr"""
    ).fetchall()

    conn.close()
    print(json.dumps({
        "nodes": [dict(r) for r in nodes],
        "edges": [dict(r) for r in edges],
    }, indent=2, default=str))


def cmd_stats(args):
    engine = get_engine()
    stats = engine.get_stats()
    print(json.dumps(stats, indent=2))


def cmd_history(args):
    engine = get_engine()
    history = engine.get_history(args.entity)
    print(json.dumps(history, indent=2))


def cmd_profile(args):
    engine = get_engine()
    profile = engine.get_profile(args.entity)
    if profile:
        print(json.dumps(profile, indent=2))
    else:
        print(f"No profile found for '{args.entity}'", file=sys.stderr)
        sys.exit(1)


def cmd_entities(args):
    import sqlite3
    conn = sqlite3.connect(DB_PATH)
    rows = conn.execute(
        "SELECT DISTINCT entity_name FROM profiles ORDER BY entity_name"
    ).fetchall()
    conn.close()
    for r in rows:
        print(r[0])


def main():
    parser = argparse.ArgumentParser(description="OpenClaw Memory Engine CLI")
    sub = parser.add_subparsers(dest="command", required=True)

    # ingest
    p = sub.add_parser("ingest", help="Extract and store memories from text")
    p.add_argument("text", help="Text to extract memories from")
    p.add_argument("--session", help="Session key (default: cli)")
    p.add_argument("--agent", help="Agent ID (default: kit)")
    p.add_argument("--date", help="Document date (YYYY-MM-DD)")
    p.set_defaults(func=cmd_ingest)

    # search
    p = sub.add_parser("search", help="Semantic search over memories")
    p.add_argument("query", help="Search query")
    p.add_argument("--top-k", type=int, default=10)
    p.add_argument("--all", action="store_true", help="Include superseded memories")
    p.add_argument("--as-of", help="Search as of date (YYYY-MM-DD)")
    p.set_defaults(func=cmd_search)

    # recall
    p = sub.add_parser("recall", help="Agent-optimized search (compact context block)")
    p.add_argument("query", help="Search query")
    p.add_argument("--top-k", type=int, default=5)
    p.set_defaults(func=cmd_recall)

    # graph
    p = sub.add_parser("graph", help="Dump full memory graph as JSON")
    p.set_defaults(func=cmd_graph)

    # stats
    p = sub.add_parser("stats", help="Memory counts and categories")
    p.set_defaults(func=cmd_stats)

    # history
    p = sub.add_parser("history", help="Version timeline for an entity")
    p.add_argument("entity", help="Entity name")
    p.set_defaults(func=cmd_history)

    # profile
    p = sub.add_parser("profile", help="Auto-built entity profile")
    p.add_argument("entity", help="Entity name")
    p.set_defaults(func=cmd_profile)

    # entities
    p = sub.add_parser("entities", help="List all known entities")
    p.set_defaults(func=cmd_entities)

    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()


path: scripts/memory.sh
content: #!/bin/bash
# Ultramemory CLI wrapper. Finds venv and API key automatically.
# Usage: bash scripts/memory.sh <command> [args...]

set -euo pipefail

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

# Require ultramemory to be pip-installed
if ! python3 -c "import ultramemory" 2>/dev/null; then
    echo "ERROR: ultramemory not found. Install with: pip install ultramemory" >&2
    exit 1
fi

# Verify API key is set (required for LLM fact extraction during ingest)
if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${OPENAI_API_KEY:-}" ]]; then
    echo "ERROR: ANTHROPIC_API_KEY or OPENAI_API_KEY must be set for fact extraction." >&2
    echo "Search/recall work without it, but ingest requires an LLM API key." >&2
    echo "Set one in your environment: export ANTHROPIC_API_KEY=sk-ant-..." >&2
    exit 1
fi

exec python3 "$SKILL_DIR/scripts/memory.py" "$@"


path: scripts/startup-recall.sh
content: #!/bin/bash
# Agent startup recall — queries ultramemory API for relevant context.
# Usage: bash startup-recall.sh [agent_id] [optional query terms...]
# Returns compact context block for prompt injection.
# Requires memory API running on localhost:8642

AGENT_ID="${1:-kit}"
shift

# Default queries if none provided
if [ $# -eq 0 ]; then
    QUERIES='["current projects and priorities","recent decisions and changes","known issues and blockers"]'
else
    # Build JSON array from args
    QUERIES="["
    for q in "$@"; do
        QUERIES="${QUERIES}\"${q}\","
    done
    QUERIES="${QUERIES%,}]"
fi

# Hit the fast startup-context endpoint (sub-100ms with warm model)
RESPONSE=$(curl -s --max-time 5 -X POST "http://localhost:8642/api/startup-context" \
    -H "Content-Type: application/json" \
    -d "{\"agent_id\":\"${AGENT_ID}\",\"queries\":${QUERIES},\"top_k_per_query\":3}" 2>/dev/null)

if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
    echo "# Supermemory: API unavailable"
    exit 0  # Don't fail agent startup if memory is down
fi

# Extract context field
echo "$RESPONSE" | python3 -c "
import sys, json
try:
    data = json.load(sys.stdin)
    ctx = data.get('context', '')
    count = data.get('memory_count', 0)
    if ctx and count > 0:
        print(f'# Supermemory Recall ({count} memories)')
        print(ctx)
    else:
        print('# Supermemory: no relevant memories found')
except:
    print('# Supermemory: parse error')
" 2>/dev/null


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