Initial resurrection kit — 81 scripts, 62 references, configs, systemd units, crons, Docker compose files, Caddy config, master README
This commit is contained in:
Executable
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""hermes-consolidate.py — Dump/prune Hermes memory.
|
||||
|
||||
Usage:
|
||||
hermes-consolidate.py --dump-only # dump MEMORY.md entries to JSON (stdout)
|
||||
hermes-consolidate.py --prune-facts # prune MEMORY.md + holographic fact store
|
||||
"""
|
||||
import json, os, re, sys, sqlite3, time
|
||||
from pathlib import Path
|
||||
|
||||
HERMES_HOME = Path(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")))
|
||||
MEMORY_MD = HERMES_HOME / "memories" / "MEMORY.md"
|
||||
FACT_DB = HERMES_HOME / "memory_store.db"
|
||||
TARGET = 5000 # target chars for MEMORY.md after prune
|
||||
PROTECT = re.compile(
|
||||
r"(Germaine's #1 rule|never make up|fabrication|dealbreaker|"
|
||||
r"Sho'Nuff Brown|shonuff@|germainebrown\\.com|itpp-infra|"
|
||||
r"DR issue log|NETCUP_PASSWORD|LoveMyBoys|"
|
||||
r"Core: netcup|STANDING PRACTICE|app1-bu|"
|
||||
r"admin-ai\\.itpropartner|RS 4000|"
|
||||
r"EMAIL: proper Sho'Nuff formatting|EMAIL FORMATTING RULE|"
|
||||
r"#1 rule|Germaine's email|Super Search|Exa API key)"
|
||||
)
|
||||
|
||||
|
||||
def load_entries():
|
||||
raw = MEMORY_MD.read_text()
|
||||
entries = [e.strip() for e in raw.split("\n§\n") if e.strip()]
|
||||
return entries
|
||||
|
||||
|
||||
def save_entries(entries):
|
||||
text = "\n§\n".join(entries) + "\n"
|
||||
tmp = MEMORY_MD.with_suffix(".md.tmp")
|
||||
tmp.write_text(text)
|
||||
tmp.replace(MEMORY_MD)
|
||||
|
||||
|
||||
def dump_only():
|
||||
entries = load_entries()
|
||||
char_count = sum(len(e) for e in entries)
|
||||
data = {
|
||||
"backed_up_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"source": str(MEMORY_MD),
|
||||
"char_limit": 10000,
|
||||
"target": TARGET,
|
||||
"entry_count": len(entries),
|
||||
"char_count": char_count,
|
||||
"entries": entries,
|
||||
}
|
||||
json.dump(data, sys.stdout, indent=2)
|
||||
print()
|
||||
|
||||
|
||||
def prune_entries(entries):
|
||||
"""Pattern-prune + size safety valve. Returns kept entries."""
|
||||
# Pattern pruning: drop obviously stale entries
|
||||
stale_patterns = [
|
||||
r"\bcomplete\b",
|
||||
r"\bdone\b",
|
||||
r"\bshipped\b",
|
||||
r"\bresolved\b",
|
||||
r"\[STALE\]",
|
||||
r"\[EXPIRED\]",
|
||||
r"\[OBSOLETE\]",
|
||||
]
|
||||
|
||||
kept = []
|
||||
for e in entries:
|
||||
# Always keep PROTECTed entries
|
||||
if PROTECT.search(e):
|
||||
kept.append(e)
|
||||
continue
|
||||
|
||||
# Drop if matches any stale pattern (single-line quick check)
|
||||
is_stale = False
|
||||
for pat in stale_patterns:
|
||||
if re.search(pat, e, re.IGNORECASE):
|
||||
is_stale = True
|
||||
break
|
||||
|
||||
# Also check for task-completion indicators
|
||||
if re.search(r"^(✅|❌|complete|done)", e.strip()):
|
||||
is_stale = True
|
||||
|
||||
if not is_stale:
|
||||
kept.append(e)
|
||||
|
||||
# Size safety valve: if still over target, drop oldest entries
|
||||
char_count = sum(len(e) for e in kept)
|
||||
if char_count > TARGET:
|
||||
# Drop from the top (oldest-appended) first, but never PROTECTed
|
||||
safe = [e for e in kept if PROTECT.search(e)]
|
||||
pruneable = [e for e in kept if not PROTECT.search(e)]
|
||||
|
||||
while pruneable and char_count > TARGET:
|
||||
removed = pruneable.pop(0) # oldest first
|
||||
char_count -= len(removed)
|
||||
|
||||
kept = safe + pruneable
|
||||
|
||||
return kept
|
||||
|
||||
|
||||
def prune_fact_store():
|
||||
"""Remove facts older than 7 days that were never retrieved and have trust < 0.5."""
|
||||
try:
|
||||
conn = sqlite3.connect(str(FACT_DB))
|
||||
c = conn.cursor()
|
||||
cutoff = time.time() - (7 * 86400)
|
||||
c.execute(
|
||||
"""DELETE FROM facts
|
||||
WHERE created_at < ?
|
||||
AND retrieval_count = 0
|
||||
AND trust_score < 0.5""",
|
||||
(cutoff,),
|
||||
)
|
||||
deleted = c.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return deleted
|
||||
except (sqlite3.Error, FileNotFoundError):
|
||||
return 0
|
||||
|
||||
|
||||
def prune_facts():
|
||||
"""Main prune entry point."""
|
||||
entries = load_entries()
|
||||
if not entries:
|
||||
print("RESULT:OK|No entries to prune")
|
||||
return
|
||||
|
||||
original_count = len(entries)
|
||||
original_chars = sum(len(e) for e in entries)
|
||||
|
||||
kept = prune_entries(entries)
|
||||
|
||||
if not kept:
|
||||
print("RESULT:FAIL|Prune would result in empty memory — ABORTED")
|
||||
sys.exit(1)
|
||||
|
||||
save_entries(kept)
|
||||
kept_count = len(kept)
|
||||
kept_chars = sum(len(e) for e in kept)
|
||||
|
||||
# Prune fact store
|
||||
facts_deleted = prune_fact_store()
|
||||
|
||||
print(f"RESULT:OK|Entries: {original_count}→{kept_count}, "
|
||||
f"Chars: {original_chars}→{kept_chars}, "
|
||||
f"Facts pruned: {facts_deleted}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--dump-only" in sys.argv:
|
||||
dump_only()
|
||||
elif "--prune-facts" in sys.argv:
|
||||
prune_facts()
|
||||
else:
|
||||
print("Usage: hermes-consolidate.py --dump-only | --prune-facts", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user