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
+80 -19
View File
@@ -2,7 +2,13 @@
# ============================================================================== # ==============================================================================
# setup-compaction-cron.sh # setup-compaction-cron.sh
# Declaratively registers the Multi-Agent Nightly Session Compaction in OpenClaw. # 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 set -euo pipefail
@@ -12,21 +18,24 @@ POD_NAME="${POD_NAME:-openclaw-0}"
CONTAINER_NAME="${CONTAINER_NAME:-openclaw}" CONTAINER_NAME="${CONTAINER_NAME:-openclaw}"
JOB_NAME="nightly-session-compaction" JOB_NAME="nightly-session-compaction"
CRON_SCHEDULE="30 3 * * *" CRON_SCHEDULE="45 4 * * *"
TIMEZONE="Europe/Berlin" 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..." 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 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 #!/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. 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. 2. Recovers any wedged or locked sessions via recover_sessions.py.
3. Compacts all active forum topics and main sessions exceeding the token threshold. 3. Prunes abandoned isolated cron sessions (>48h old) to prevent token/state bloat.
4. Checkpoints SQLite WAL journals across all agent databases to disk. 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 glob
import json import json
@@ -42,6 +51,14 @@ THRESHOLD_EVENT_COUNT = 40
CRON_MAX_AGE_HOURS = 48 CRON_MAX_AGE_HOURS = 48
RPC_TIMEOUT_MS = 180000 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(): def prewarm_page_cache():
print("=== Pre-warming SQLite databases into OS page cache ===") print("=== Pre-warming SQLite databases into OS page cache ===")
t0 = time.time() 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.") 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): def get_session_metrics(agent, key, sid=None):
db_path = f"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite" db_path = f"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite"
if not os.path.exists(db_path): if not os.path.exists(db_path):
@@ -151,7 +194,8 @@ def get_session_metrics(agent, key, sid=None):
return 0, 0 return 0, 0
def compact_active_sessions(): 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) res = subprocess.run(["openclaw", "sessions", "--all-agents", "--json"], capture_output=True, text=True)
if res.returncode != 0: if res.returncode != 0:
print(f"Failed to list sessions: {res.stderr}") print(f"Failed to list sessions: {res.stderr}")
@@ -167,6 +211,10 @@ def compact_active_sessions():
tokens = s.get("totalTokens") or 0 tokens = s.get("totalTokens") or 0
sid = s.get("sessionId") 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) # Target main sessions and all telegram sessions (topics and direct)
is_target = ( is_target = (
key == "agent:main:main" or key == "agent:main:main" or
@@ -204,7 +252,7 @@ def compact_active_sessions():
compacted += 1 compacted += 1
else: else:
print(f" ✗ Fallback failed: {fb_res.stderr.strip()}") print(f" ✗ Fallback failed: {fb_res.stderr.strip()}")
time.sleep(2) time.sleep(5) # 5s pause to avoid rotational HDD I/O saturation
else: else:
skipped += 1 skipped += 1
@@ -228,17 +276,16 @@ def checkpoint_wal_journals():
print(f"WAL Checkpoint complete: {wal_freed / (1024*1024):.2f} MB merged to disk.") print(f"WAL Checkpoint complete: {wal_freed / (1024*1024):.2f} MB merged to disk.")
def vacuum_and_optimize(): 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") dbs = ["/home/node/.openclaw/state/openclaw.sqlite"] + glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")
for db in dbs: for db in dbs:
try: try:
con = sqlite3.connect(db, timeout=60.0) con = sqlite3.connect(db, timeout=30.0)
con.execute("PRAGMA optimize;") con.execute("PRAGMA optimize;")
con.execute("VACUUM;")
con.close() con.close()
except Exception as e: except Exception as e:
print(f" ✗ Error vacuuming {db}: {e}") print(f" ✗ Error optimizing {db}: {e}")
print("SQLite VACUUM and optimization complete.") print("SQLite optimization complete.")
def main(): def main():
prewarm_page_cache() prewarm_page_cache()
@@ -247,6 +294,7 @@ def main():
except Exception: except Exception:
pass pass
prune_stale_cron_sessions() prune_stale_cron_sessions()
reset_tier_a_sessions()
compact_active_sessions() compact_active_sessions()
checkpoint_wal_journals() checkpoint_wal_journals()
vacuum_and_optimize() vacuum_and_optimize()
@@ -258,8 +306,18 @@ chmod +x /home/node/.openclaw/scripts/compact_sessions.py
' '
# 2. Register / Update the OpenClaw cron job # 2. Register / Update the OpenClaw cron job
echo "Registering OpenClaw Cron automation '$JOB_NAME' ($CRON_SCHEDULE @ $TIMEZONE)..." echo "Checking existing OpenClaw Cron automation '$JOB_NAME'..."
kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron add \ 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)
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" \ --name "Nightly Session Compaction" \
--declaration-key "$JOB_NAME" \ --declaration-key "$JOB_NAME" \
--description "Nightly multi-agent session compaction and SQLite WAL checkpointing" \ --description "Nightly multi-agent session compaction and SQLite WAL checkpointing" \
@@ -268,6 +326,9 @@ kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron a
--session "isolated" \ --session "isolated" \
--no-deliver \ --no-deliver \
--command "python3 /home/node/.openclaw/scripts/compact_sessions.py" --command "python3 /home/node/.openclaw/scripts/compact_sessions.py"
fi
echo "=== Compaction Cron automation successfully registered! ===" echo "=== Compaction Cron automation successfully registered/updated! ==="
kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron show "$JOB_NAME" 2>/dev/null || true if [ -n "$EXISTING_ID" ]; then
kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron get "$EXISTING_ID" 2>/dev/null || true
fi