feat(openclaw): implement two-tier session maintenance with nightly resets for state-externalized agents

This commit is contained in:
Moritz Graf
2026-10-09 10:36:43 +02:00
parent 4a63e273fc
commit 45562c23d9
2 changed files with 89 additions and 28 deletions
+88 -27
View File
@@ -2,7 +2,13 @@
# ==============================================================================
# setup-compaction-cron.sh
# Declaratively registers the Multi-Agent Nightly Session Compaction in OpenClaw.
# Schedule: Every night at 03:30 Europe/Berlin (cron: 30 3 * * *)
# Schedule: Every night at 04:45 Europe/Berlin (02:45 UTC) (cron: 45 4 * * *)
#
# Architecture:
# - Tier A: Daily Hard-Reset to 0 for koch (Topic 21), hebamme (Topic 22), shopping (Topic 115)
# (100% externalized state in DuckDB/JSONL; resets prevent cross-day hallucination)
# - Tier B: Smart Compaction for long-running project agents (itguy, recherche, regensburg, fitness, doktor)
# - WAL checkpoints and SQLite optimization without blocking full VACUUM
# ==============================================================================
set -euo pipefail
@@ -12,21 +18,24 @@ POD_NAME="${POD_NAME:-openclaw-0}"
CONTAINER_NAME="${CONTAINER_NAME:-openclaw}"
JOB_NAME="nightly-session-compaction"
CRON_SCHEDULE="30 3 * * *"
CRON_SCHEDULE="45 4 * * *"
TIMEZONE="Europe/Berlin"
echo "=== Registering Multi-Agent Nightly Session Compaction in OpenClaw ($POD_NAME) ==="
echo "=== Registering Multi-Agent Nightly Session Maintenance in OpenClaw ($POD_NAME) ==="
# 1. Install or ensure python compaction script is present on PVC
# 1. Install or ensure python maintenance script is present on PVC
echo "Ensuring /home/node/.openclaw/scripts/compact_sessions.py exists..."
kubectl exec -i -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- /bin/sh -c 'cat << "EOF" > /home/node/.openclaw/scripts/compact_sessions.py
#!/usr/bin/env python3
"""
Nightly multi-agent session compaction and maintenance script for OpenClaw.
Nightly multi-agent session maintenance and compaction script for OpenClaw.
1. Pre-warms OS page cache for SQLite databases on spinning disks.
2. Prunes abandoned isolated cron sessions (>48h old) to prevent token/state bloat.
3. Compacts all active forum topics and main sessions exceeding the token threshold.
4. Checkpoints SQLite WAL journals across all agent databases to disk.
2. Recovers any wedged or locked sessions via recover_sessions.py.
3. Prunes abandoned isolated cron sessions (>48h old) to prevent token/state bloat.
4. Tier A Hard-Reset: Resets koch, hebamme, and shopping topics to 0 tokens (clean archive).
5. Tier B Smart-Compaction: Compacts active sessions exceeding token thresholds (5s pacing).
6. Checkpoints SQLite WAL journals across all agent databases to disk.
7. Optimizes SQLite query planner without blocking full VACUUM.
"""
import glob
import json
@@ -42,6 +51,14 @@ THRESHOLD_EVENT_COUNT = 40
CRON_MAX_AGE_HOURS = 48
RPC_TIMEOUT_MS = 180000
# Tier A: Event-logging agents whose state is 100% externalized (DuckDB / JSONL / Groceries DB).
# Nightly reset to 0 prevents cross-day hallucination and eliminates LLM compaction token costs.
TIER_A_RESET_TARGETS = [
("koch", "agent:koch:telegram:group:-1004326921510:topic:21"),
("hebamme", "agent:hebamme:telegram:group:-1004326921510:topic:22"),
("shopping", "agent:shopping:telegram:group:-1004326921510:topic:115"),
]
def prewarm_page_cache():
print("=== Pre-warming SQLite databases into OS page cache ===")
t0 = time.time()
@@ -128,6 +145,32 @@ def prune_stale_cron_sessions():
print(f"Cron pruning complete: {total_pruned} sessions pruned, {total_tokens_reclaimed} tokens reclaimed.")
def reset_tier_a_sessions():
print("=== Tier A: Daily Hard-Reset to 0 (koch, hebamme, shopping) ===")
for agent, key in TIER_A_RESET_TARGETS:
db_path = f"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite"
if not os.path.exists(db_path):
continue
try:
con = sqlite3.connect(db_path, timeout=10.0)
row = con.execute("SELECT current_session_id FROM session_nodes WHERE session_key=?", (key,)).fetchone()
con.close()
if not row or not row[0]:
print(f" [Tier A] {agent} ({key}) already at 0 (no active session).")
continue
print(f" [Tier A] Resetting {agent} ({key})...")
r = subprocess.run(
["openclaw", "sessions", "delete", key, "--agent", agent, "--yes"],
capture_output=True, text=True, timeout=60.0
)
if r.returncode == 0:
print(f" ✓ Reset success: {r.stdout.strip()}")
else:
print(f" ✗ Reset error: {r.stderr.strip()}")
except Exception as e:
print(f" ✗ Exception resetting {agent}: {e}")
time.sleep(1)
def get_session_metrics(agent, key, sid=None):
db_path = f"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite"
if not os.path.exists(db_path):
@@ -151,7 +194,8 @@ def get_session_metrics(agent, key, sid=None):
return 0, 0
def compact_active_sessions():
print(f"=== Running OpenClaw Multi-Agent Compaction (threshold: {THRESHOLD_TOKENS} tokens / {THRESHOLD_TRANSCRIPT_BYTES//1024}KB, LLM summarization) ===")
tier_a_keys = set(t[1] for t in TIER_A_RESET_TARGETS)
print(f"=== Tier B: Running Multi-Agent Compaction (threshold: {THRESHOLD_TOKENS} tokens / {THRESHOLD_TRANSCRIPT_BYTES//1024}KB, LLM summarization) ===")
res = subprocess.run(["openclaw", "sessions", "--all-agents", "--json"], capture_output=True, text=True)
if res.returncode != 0:
print(f"Failed to list sessions: {res.stderr}")
@@ -167,6 +211,10 @@ def compact_active_sessions():
tokens = s.get("totalTokens") or 0
sid = s.get("sessionId")
# Skip Tier A agents as they are handled via hard-reset
if key in tier_a_keys:
continue
# Target main sessions and all telegram sessions (topics and direct)
is_target = (
key == "agent:main:main" or
@@ -204,7 +252,7 @@ def compact_active_sessions():
compacted += 1
else:
print(f" ✗ Fallback failed: {fb_res.stderr.strip()}")
time.sleep(2)
time.sleep(5) # 5s pause to avoid rotational HDD I/O saturation
else:
skipped += 1
@@ -228,17 +276,16 @@ def checkpoint_wal_journals():
print(f"WAL Checkpoint complete: {wal_freed / (1024*1024):.2f} MB merged to disk.")
def vacuum_and_optimize():
print("=== Running SQLite VACUUM and OPTIMIZE across all databases ===")
print("=== Running SQLite OPTIMIZE across all databases ===")
dbs = ["/home/node/.openclaw/state/openclaw.sqlite"] + glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")
for db in dbs:
try:
con = sqlite3.connect(db, timeout=60.0)
con = sqlite3.connect(db, timeout=30.0)
con.execute("PRAGMA optimize;")
con.execute("VACUUM;")
con.close()
except Exception as e:
print(f" ✗ Error vacuuming {db}: {e}")
print("SQLite VACUUM and optimization complete.")
print(f" ✗ Error optimizing {db}: {e}")
print("SQLite optimization complete.")
def main():
prewarm_page_cache()
@@ -247,6 +294,7 @@ def main():
except Exception:
pass
prune_stale_cron_sessions()
reset_tier_a_sessions()
compact_active_sessions()
checkpoint_wal_journals()
vacuum_and_optimize()
@@ -258,16 +306,29 @@ chmod +x /home/node/.openclaw/scripts/compact_sessions.py
'
# 2. Register / Update the OpenClaw cron job
echo "Registering OpenClaw Cron automation '$JOB_NAME' ($CRON_SCHEDULE @ $TIMEZONE)..."
kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron add \
--name "Nightly Session Compaction" \
--declaration-key "$JOB_NAME" \
--description "Nightly multi-agent session compaction and SQLite WAL checkpointing" \
--cron "$CRON_SCHEDULE" \
--tz "$TIMEZONE" \
--session "isolated" \
--no-deliver \
--command "python3 /home/node/.openclaw/scripts/compact_sessions.py"
echo "Checking existing OpenClaw Cron automation '$JOB_NAME'..."
EXISTING_ID=$(kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron list --json 2>/dev/null | python3 -c 'import sys, json; data=json.load(sys.stdin); print(next((j["id"] for j in data.get("jobs", []) if j.get("declarationKey") == "'"$JOB_NAME"'"), ""))' 2>/dev/null || true)
echo "=== Compaction Cron automation successfully registered! ==="
kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron show "$JOB_NAME" 2>/dev/null || true
if [ -n "$EXISTING_ID" ]; then
echo "Updating existing OpenClaw Cron automation '$EXISTING_ID' ($CRON_SCHEDULE @ $TIMEZONE)..."
kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron edit "$EXISTING_ID" \
--cron "$CRON_SCHEDULE" \
--tz "$TIMEZONE" \
--command "python3 /home/node/.openclaw/scripts/compact_sessions.py"
else
echo "Registering new OpenClaw Cron automation '$JOB_NAME' ($CRON_SCHEDULE @ $TIMEZONE)..."
kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron add \
--name "Nightly Session Compaction" \
--declaration-key "$JOB_NAME" \
--description "Nightly multi-agent session compaction and SQLite WAL checkpointing" \
--cron "$CRON_SCHEDULE" \
--tz "$TIMEZONE" \
--session "isolated" \
--no-deliver \
--command "python3 /home/node/.openclaw/scripts/compact_sessions.py"
fi
echo "=== Compaction Cron automation successfully registered/updated! ==="
if [ -n "$EXISTING_ID" ]; then
kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron get "$EXISTING_ID" 2>/dev/null || true
fi