MP-1 (#368): Port PalaceRoom + Mempalace classes with 22 unit tests MP-2 (#369): L0-L5 retrieval order enforcer with recall-query detection MP-5 (#372): Wake-up protocol (300-900 token context), session scratchpad Modules: - mempalace.py: PalaceRoom + Mempalace dataclasses, factory constructors - retrieval_enforcer.py: Layered memory retrieval (identity → palace → scratch → gitea → skills) - wakeup.py: Session wake-up with caching (5min TTL) - scratchpad.py: JSON-based session notes with palace promotion All 65 tests pass. Pure stdlib + graceful degradation for ONNX issues (#373).
162 lines
4.9 KiB
Python
162 lines
4.9 KiB
Python
"""Wake-up Protocol — session start context injection.
|
|
|
|
Generates 300-900 tokens of context when a new Hermes session starts.
|
|
Loads identity, recent palace context, and fleet status.
|
|
|
|
Refs: Epic #367, Sub-issue #372
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
IDENTITY_PATH = Path.home() / ".mempalace" / "identity.txt"
|
|
MEMPALACE_BIN = "/Library/Frameworks/Python.framework/Versions/3.12/bin/mempalace"
|
|
FLEET_STATUS_PATH = Path.home() / ".hermes" / "fleet_status.json"
|
|
WAKEUP_CACHE_PATH = Path.home() / ".hermes" / "last_wakeup.txt"
|
|
WAKEUP_CACHE_TTL = 300 # 5 minutes — don't regenerate if recent
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _load_identity() -> str:
|
|
"""Read the agent identity file."""
|
|
try:
|
|
if IDENTITY_PATH.exists():
|
|
text = IDENTITY_PATH.read_text(encoding="utf-8").strip()
|
|
# Cap at ~150 tokens for wake-up brevity
|
|
words = text.split()
|
|
if len(words) > 150:
|
|
text = " ".join(words[:150]) + "..."
|
|
return text
|
|
except (OSError, PermissionError):
|
|
pass
|
|
return ""
|
|
|
|
|
|
def _palace_context() -> str:
|
|
"""Run mempalace wake-up command for recent context. Degrades gracefully."""
|
|
try:
|
|
bin_path = MEMPALACE_BIN if os.path.exists(MEMPALACE_BIN) else "mempalace"
|
|
result = subprocess.run(
|
|
[bin_path, "wake-up"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
return result.stdout.strip()
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
# ONNX issues (#373) or CLI not available — degrade gracefully
|
|
pass
|
|
return ""
|
|
|
|
|
|
def fleet_status_summary() -> str:
|
|
"""Read cached fleet status for lightweight session context."""
|
|
try:
|
|
if FLEET_STATUS_PATH.exists():
|
|
data = json.loads(FLEET_STATUS_PATH.read_text(encoding="utf-8"))
|
|
lines = ["## Fleet Status"]
|
|
|
|
if isinstance(data, dict):
|
|
for agent, status in data.items():
|
|
if isinstance(status, dict):
|
|
state = status.get("state", "unknown")
|
|
last_seen = status.get("last_seen", "?")
|
|
lines.append(f" {agent}: {state} (last: {last_seen})")
|
|
else:
|
|
lines.append(f" {agent}: {status}")
|
|
|
|
if len(lines) > 1:
|
|
return "\n".join(lines)
|
|
except (OSError, json.JSONDecodeError):
|
|
pass
|
|
return ""
|
|
|
|
|
|
def _check_cache() -> str:
|
|
"""Return cached wake-up if fresh enough."""
|
|
try:
|
|
if WAKEUP_CACHE_PATH.exists():
|
|
age = time.time() - WAKEUP_CACHE_PATH.stat().st_mtime
|
|
if age < WAKEUP_CACHE_TTL:
|
|
return WAKEUP_CACHE_PATH.read_text(encoding="utf-8").strip()
|
|
except OSError:
|
|
pass
|
|
return ""
|
|
|
|
|
|
def _write_cache(content: str) -> None:
|
|
"""Cache the wake-up content."""
|
|
try:
|
|
WAKEUP_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
WAKEUP_CACHE_PATH.write_text(content, encoding="utf-8")
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def palace_wakeup(force: bool = False) -> str:
|
|
"""Generate wake-up context for a new session. ~300-900 tokens.
|
|
|
|
Args:
|
|
force: If True, bypass the 5-minute cache and regenerate.
|
|
|
|
Returns:
|
|
Formatted context string suitable for prepending to the system prompt.
|
|
"""
|
|
# Check cache first (avoids redundant work on rapid session restarts)
|
|
if not force:
|
|
cached = _check_cache()
|
|
if cached:
|
|
return cached
|
|
|
|
parts = []
|
|
|
|
# L0: Identity
|
|
identity = _load_identity()
|
|
if identity:
|
|
parts.append(f"## Identity\n{identity}")
|
|
|
|
# L1: Recent palace context
|
|
palace = _palace_context()
|
|
if palace:
|
|
parts.append(palace)
|
|
|
|
# Fleet status (lightweight)
|
|
fleet = fleet_status_summary()
|
|
if fleet:
|
|
parts.append(fleet)
|
|
|
|
# Timestamp
|
|
parts.append(f"## Session\nWake-up generated: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
content = "\n\n".join(parts)
|
|
|
|
# Cache for TTL
|
|
_write_cache(content)
|
|
|
|
return content
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI entry point for testing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if __name__ == "__main__":
|
|
print(palace_wakeup(force=True))
|