feat(openclaw): implement two-tier session maintenance with nightly resets for state-externalized agents
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
Submodule knowledge-wiki updated: 10d024da2d...12b50535a6
Reference in New Issue
Block a user