darkplex-core/cortex/memory/pre_compaction.py
Claudia 0484c6321a
Some checks failed
Tests / test (push) Failing after 5s
feat(memory): add session memory persistence module
New cortex/memory/ module that provides:
- boot_assembler: builds BOOTSTRAP.md from threads, decisions, narrative
- thread_tracker: tracks conversation threads across sessions via NATS
- narrative_generator: daily narrative with Ollama LLM (fallback: structured)
- pre_compaction: snapshot pipeline before context compaction

CLI commands:
- cortex memory bootstrap [--dry-run] [--workspace DIR]
- cortex memory snapshot [--workspace DIR]
- cortex memory threads [--summary] [--hours N]

All paths configurable via WORKSPACE_DIR, NATS_URL, AGENT_NAME env vars.
No hardcoded paths. Works with any OpenClaw agent.

Fixes array/dict handling for empty threads.json and decisions.json.
2026-02-13 11:52:25 +01:00

110 lines
3.7 KiB
Python

"""Pre-Compaction Snapshot — Captures the "hot zone" before memory loss.
Orchestrates: thread tracker → hot snapshot → narrative → boot assembler.
"""
import json
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from .common import get_workspace_dir, get_reboot_dir, get_nats_env
from .thread_tracker import run as run_thread_tracker
from .narrative_generator import run as run_narrative
from .boot_assembler import run as run_boot_assembler
def fetch_recent_messages(count: int = 20, workspace: Path = None) -> list[str]:
"""Fetch last N messages from NATS for snapshot."""
ws = workspace or get_workspace_dir()
env = get_nats_env(ws)
messages = []
try:
result = subprocess.run(
["nats", "stream", "get", "openclaw-events", "--last", str(count), "--raw"],
capture_output=True, text=True, timeout=15, env=env
)
if result.returncode == 0 and result.stdout.strip():
for line in result.stdout.strip().split("\n"):
try:
evt = json.loads(line)
content = evt.get("content", evt.get("message", evt.get("text", "")))
sender = evt.get("sender", evt.get("agent", evt.get("role", "?")))
if content and len(content.strip()) > 3:
short = content.strip()[:200]
if len(content.strip()) > 200:
short += "..."
messages.append(f"[{sender}] {short}")
except json.JSONDecodeError:
continue
except Exception as e:
messages.append(f"(NATS fetch failed: {e})")
return messages
def build_snapshot(messages: list[str]) -> str:
"""Build the hot snapshot markdown."""
now = datetime.now(timezone.utc)
parts = [
f"# Hot Snapshot — {now.isoformat()[:19]}Z",
"## Last ~30min before compaction",
"",
]
if messages:
parts.append("**Recent conversation:**")
for msg in messages[-15:]:
parts.append(f"- {msg}")
else:
parts.append("(No recent messages captured)")
parts.append("")
return "\n".join(parts)
def run(dry_run: bool = False, workspace: Path = None, **assembler_kwargs):
"""Run the full pre-compaction pipeline."""
ws = workspace or get_workspace_dir()
reboot_dir = get_reboot_dir(ws)
hot_snapshot_file = reboot_dir / "hot-snapshot.md"
print("🔥 Pre-Compaction Snapshot — capturing hot zone...")
# 1. Thread tracker (last 1h)
print(" 1/4 Thread tracker (last 1h)...")
run_thread_tracker(hours=1, workspace=ws)
# 2. Hot snapshot
print(" 2/4 Capturing recent messages...")
messages = fetch_recent_messages(20, ws)
snapshot = build_snapshot(messages)
if dry_run:
print(snapshot)
else:
hot_snapshot_file.write_text(snapshot)
print(f" ✅ hot-snapshot.md written ({len(snapshot)} chars)")
# 3. Narrative (no LLM for speed during compaction)
print(" 3/4 Narrative generator...")
run_narrative(no_llm=True, workspace=ws)
# 4. Boot assembler
print(" 4/4 Boot assembler...")
run_boot_assembler(workspace=ws, **assembler_kwargs)
print("🔥 Pre-Compaction Snapshot — done!")
def main():
import argparse
parser = argparse.ArgumentParser(description="Pre-Compaction Snapshot")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--workspace", type=str, help="Workspace directory")
args = parser.parse_args()
ws = Path(args.workspace) if args.workspace else None
run(dry_run=args.dry_run, workspace=ws)
if __name__ == "__main__":
main()