diff --git a/AGENTS.md b/AGENTS.md index 6576512..c2a74f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,12 +94,30 @@ This repository is divided into distinct domains, each with its own `AGENTS.md` * **Key Tools**: Interactive `uv run flash.py` wizard and `./send_message.sh` CLI push messaging. * **Read**: [projects/esphome_display_kitchen/AGENTS.md](./projects/esphome_display_kitchen/AGENTS.md) -### 12. Utility Scripts (`scripts/`) +### 12. [DuckDB Backend](./projects/duckdb-backend/AGENTS.md) (`projects/duckdb-backend/`) +* **Purpose**: High-performance HTTP analytics and event tracking data service executing in-memory DuckDB queries against Knowledge Wiki JSONL datasets. +* **Manifests**: `k8s/family/` (`duckdb-backend.yaml`, `pvc.yaml`, `ingress.yaml` in namespace `family`). +* **Endpoints**: Curated dashboards (`/duckdb-api/v1/mila/dashboard`, `/duckdb-api/v1/moritz/dashboard`), generic SQL queries (`/duckdb-api/v1/query`), and event tracking writes with Git sync. +* **Read**: [projects/duckdb-backend/AGENTS.md](./projects/duckdb-backend/AGENTS.md) + +### 13. [Mila Tracking Web App](./projects/mila/AGENTS.md) (`projects/mila/`) +* **Purpose**: Reactive mobile-first PWA for Mila's baby sleep, feeding, diaper tracking, and growth percentiles (`https://family.haumdaucher.de/tracking/mila`). +* **Manifest**: `k8s/family/mila.yaml` (Namespace: `family`). +* **Key Rules**: Vue 3, Vite, TypeScript, and Tailwind CSS; live wake timer; interactive 1-tap and custom-time tracking modal. +* **Read**: [projects/mila/AGENTS.md](./projects/mila/AGENTS.md) + +### 14. [Moritz Health & Nutrition Web App](./projects/moritz/AGENTS.md) (`projects/moritz/`) +* **Purpose**: Apple Health-inspired nutrition and metabolic balance dashboard (`https://family.haumdaucher.de/tracking/moritz`). +* **Manifest**: `k8s/family/moritz.yaml` (Namespace: `family`). +* **Key Rules**: Vue 3, Vite, TypeScript, and Tailwind CSS; net calorie balance hero, Garmin health breakdown, and quick meal logger. +* **Read**: [projects/moritz/AGENTS.md](./projects/moritz/AGENTS.md) + +### 15. Utility Scripts (`scripts/`) * **Purpose**: Cross-cutting operational and administration CLI scripts that do not belong inside Kubernetes namespace directories. * **Key Tools**: * `scripts/htpasswd.py`: Generates bcrypt-hashed htpasswd credentials for ingress basic auth secrets. -### 13. Network & Router Integration (`fritzbox/`) +### 16. Network & Router Integration (`fritzbox/`) * **Purpose**: Site-to-Site WireGuard LAN coupling and DynDNS OVH synchronization for the home FritzBox router. * **Cluster Endpoint**: `k8s/wireguard/` (`vpn.haumdaucher.de`). * **Read**: [fritzbox/README.md](./fritzbox/README.md) diff --git a/k8s/openclaw/openclaw-instance.secret.yaml b/k8s/openclaw/openclaw-instance.secret.yaml index aa17185..bdb539e 100644 Binary files a/k8s/openclaw/openclaw-instance.secret.yaml and b/k8s/openclaw/openclaw-instance.secret.yaml differ diff --git a/k8s/openclaw/scripts/compact_sessions.py b/k8s/openclaw/scripts/compact_sessions.py index 544bed5..fd50c46 100644 --- a/k8s/openclaw/scripts/compact_sessions.py +++ b/k8s/openclaw/scripts/compact_sessions.py @@ -23,6 +23,15 @@ THRESHOLD_EVENT_COUNT = 30 CRON_MAX_AGE_HOURS = 48 RPC_TIMEOUT_MS = 180000 +# Tracking Channels: Deterministic sliding-window (mila_tracker, nutrition_tracker). +# Strict sliding window of ~5-8 events (2-3 turns max), 0 LLM tokens, instant truncation. +TRACKING_TARGETS = [ + ("mila_tracker", "agent:mila_tracker:telegram:group:-1004326921510:topic:2796"), + ("nutrition_tracker", "agent:nutrition_tracker:telegram:group:-1004326921510:topic:2797"), +] +MAX_TRACKING_EVENTS = 8 +MAX_TRACKING_LINES = 8 + # Tier A: High-frequency interaction topics (koch, hebamme, shopping). # Deep-compaction keeps a compact summary + latest 2 turns without completely wiping conversational memory. TIER_A_TARGETS = [ @@ -117,6 +126,41 @@ def prune_stale_cron_sessions(): print(f"Cron pruning complete: {total_pruned} sessions pruned, {total_tokens_reclaimed} tokens reclaimed.") +def compact_tracking_sessions(): + print("=== Tracking Channels Sliding-Window Maintenance (mila_tracker, nutrition_tracker) ===") + for agent, key in TRACKING_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=5.0) + row = con.execute("SELECT current_session_id FROM session_nodes WHERE session_key=?", (key,)).fetchone() + if not row or not row[0]: + con.close() + print(f" [{agent}] No active session node found for {key}.") + continue + sid = row[0] + cnt = con.execute("SELECT count(*) FROM transcript_events WHERE session_id=?", (sid,)).fetchone()[0] + con.close() + + if cnt > MAX_TRACKING_EVENTS: + print(f" [{agent}] Session {key} has {cnt} events (> {MAX_TRACKING_EVENTS}). Truncating to last {MAX_TRACKING_LINES} lines...") + cmd = [ + "openclaw", "sessions", "compact", key, + "--agent", agent, + "--max-lines", str(MAX_TRACKING_LINES), + "--timeout", "30000" + ] + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode == 0: + print(f" ✓ Truncated successfully: {r.stdout.strip()}") + else: + print(f" ✗ Truncation failed: {r.stderr.strip()}") + else: + print(f" [{agent}] Session {key} has {cnt} events (<= {MAX_TRACKING_EVENTS}). Clean.") + except Exception as e: + print(f" ✗ [{agent}] Error during tracking compaction: {e}") + def compact_tier_a_sessions(): print("=== Tier A: Deep-Compaction (koch, hebamme, shopping) ===") for agent, key in TIER_A_TARGETS: @@ -180,7 +224,7 @@ def get_session_metrics(agent, key, sid=None): return 0, 0 def compact_active_sessions(): - tier_a_keys = set(t[1] for t in TIER_A_TARGETS) + excluded_keys = set(t[1] for t in TIER_A_TARGETS) | set(t[1] for t in TRACKING_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: @@ -197,8 +241,8 @@ 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: + # Skip Tier A and Tracking agents as they have dedicated routines + if key in excluded_keys: continue # Target main sessions and all telegram sessions (topics and direct) @@ -280,6 +324,7 @@ def main(): except Exception: pass prune_stale_cron_sessions() + compact_tracking_sessions() compact_tier_a_sessions() compact_active_sessions() checkpoint_wal_journals() diff --git a/k8s/openclaw/scripts/compact_tracking_sessions.py b/k8s/openclaw/scripts/compact_tracking_sessions.py new file mode 100644 index 0000000..239cc03 --- /dev/null +++ b/k8s/openclaw/scripts/compact_tracking_sessions.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +Lightweight sliding-window compaction for OpenClaw tracking channels: +- mila_tracker (topic 2796) +- nutrition_tracker (topic 2797) + +Maintains a strict sliding window of ~5-8 transcript events (max 2-3 turns). +Uses native gateway truncation (--max-lines 8) requiring 0 LLM tokens and 0 external API calls. +""" +import os +import sqlite3 +import subprocess +import sys +import time + +MAX_TRACKING_EVENTS = 8 +MAX_TRACKING_LINES = 8 +TIMEOUT_MS = 30000 + +TRACKING_TARGETS = [ + ("mila_tracker", "agent:mila_tracker:telegram:group:-1004326921510:topic:2796"), + ("nutrition_tracker", "agent:nutrition_tracker:telegram:group:-1004326921510:topic:2797"), +] + +def check_and_compact_tracking(): + compacted_count = 0 + skipped_count = 0 + + for agent, key in TRACKING_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=5.0) + row = con.execute("SELECT current_session_id FROM session_nodes WHERE session_key=?", (key,)).fetchone() + if not row or not row[0]: + con.close() + continue + sid = row[0] + cnt = con.execute("SELECT count(*) FROM transcript_events WHERE session_id=?", (sid,)).fetchone()[0] + con.close() + + if cnt > MAX_TRACKING_EVENTS: + print(f"[{agent}] Session {key} has {cnt} events (threshold {MAX_TRACKING_EVENTS}). Truncating to last {MAX_TRACKING_LINES} lines...") + cmd = [ + "openclaw", "sessions", "compact", key, + "--agent", agent, + "--max-lines", str(MAX_TRACKING_LINES), + "--timeout", str(TIMEOUT_MS) + ] + res = subprocess.run(cmd, capture_output=True, text=True) + if res.returncode == 0: + print(f" ✓ [{agent}] Truncated successfully: {res.stdout.strip()}") + compacted_count += 1 + else: + print(f" ✗ [{agent}] Truncation failed: {res.stderr.strip()}", file=sys.stderr) + else: + skipped_count += 1 + except Exception as e: + print(f"[{agent}] Error checking session: {e}", file=sys.stderr) + + if compacted_count > 0: + print(f"Tracking maintenance: {compacted_count} sessions truncated, {skipped_count} within window.") + +if __name__ == "__main__": + check_and_compact_tracking() diff --git a/k8s/openclaw/setup-compaction-cron.sh b/k8s/openclaw/setup-compaction-cron.sh index 7c57792..ae80672 100755 --- a/k8s/openclaw/setup-compaction-cron.sh +++ b/k8s/openclaw/setup-compaction-cron.sh @@ -1,14 +1,16 @@ #!/usr/bin/env bash # ============================================================================== # setup-compaction-cron.sh -# Declaratively registers the Multi-Agent Nightly Session Compaction in OpenClaw. -# Schedule: Every night at 04:45 Europe/Berlin (02:45 UTC) (cron: 45 4 * * *) -# -# Architecture: -# - Tier A: Deep-Compaction for high-frequency topics (koch, hebamme, shopping) -# (maintains conversational continuity while keeping token count very low) -# - Tier B: Smart Compaction for long-running project agents (itguy, recherche, regensburg, fitness, doktor) -# - WAL checkpoints and SQLite optimization without blocking full VACUUM +# Declaratively registers OpenClaw session maintenance automations: +# 1. Tracking Sliding Window (every 15m): Truncates mila_tracker & nutrition_tracker +# to <= 8 events (~5 messages / 2 turns) via instant native gateway truncation. +# 2. Nightly Deep Compaction (every night at 04:45 Europe/Berlin): +# - Pre-warms SQLite page cache on rotational storage +# - Recovers wedged/quarantined sessions +# - Prunes abandoned isolated cron sessions (>48h) +# - Deep-compacts conversational topics (koch, hebamme, shopping) +# - Smart-compacts active long-running sessions (itguy, recherche, etc.) +# - Flushes SQLite WAL journals and executes PRAGMA optimize # ============================================================================== set -euo pipefail @@ -16,326 +18,62 @@ set -euo pipefail NAMESPACE="${NAMESPACE:-openclaw}" POD_NAME="${POD_NAME:-openclaw-0}" CONTAINER_NAME="${CONTAINER_NAME:-openclaw}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -JOB_NAME="nightly-session-compaction" +echo "=== Registering Multi-Agent Session Maintenance in OpenClaw ($POD_NAME) ===" + +# 1. Install maintenance scripts from repo onto PVC +echo "Syncing compact_tracking_sessions.py to pod..." +kubectl exec -i -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- /bin/sh -c \ + 'cat > /home/node/.openclaw/scripts/compact_tracking_sessions.py && chmod +x /home/node/.openclaw/scripts/compact_tracking_sessions.py' \ + < "$SCRIPT_DIR/scripts/compact_tracking_sessions.py" + +echo "Syncing compact_sessions.py to pod..." +kubectl exec -i -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- /bin/sh -c \ + 'cat > /home/node/.openclaw/scripts/compact_sessions.py && chmod +x /home/node/.openclaw/scripts/compact_sessions.py' \ + < "$SCRIPT_DIR/scripts/compact_sessions.py" + +# 2. Register / Update 15-Minute Tracking Sliding Window Automation +TRACKING_JOB="tracking-session-sliding-window" +echo "Checking existing OpenClaw Cron automation '$TRACKING_JOB'..." +EXISTING_TRACKING_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") == "'"$TRACKING_JOB"'"), ""))' 2>/dev/null || true) + +if [ -n "$EXISTING_TRACKING_ID" ]; then + echo "Updating existing tracking sliding window automation '$EXISTING_TRACKING_ID' (every 15m)..." + kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron edit "$EXISTING_TRACKING_ID" \ + --every "15m" \ + --command "python3 /home/node/.openclaw/scripts/compact_tracking_sessions.py" +else + echo "Registering new tracking sliding window automation '$TRACKING_JOB' (every 15m)..." + kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron add \ + --name "Tracking Session Sliding Window" \ + --declaration-key "$TRACKING_JOB" \ + --description "Lightweight sliding-window truncation (max 8 events) for mila_tracker and nutrition_tracker" \ + --every "15m" \ + --session "isolated" \ + --no-deliver \ + --command "python3 /home/node/.openclaw/scripts/compact_tracking_sessions.py" +fi + +# 3. Register / Update Nightly Deep Compaction Automation +NIGHTLY_JOB="nightly-session-compaction" CRON_SCHEDULE="45 4 * * *" TIMEZONE="Europe/Berlin" -echo "=== Registering Multi-Agent Nightly Session Maintenance in OpenClaw ($POD_NAME) ===" +echo "Checking existing OpenClaw Cron automation '$NIGHTLY_JOB'..." +EXISTING_NIGHTLY_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") == "'"$NIGHTLY_JOB"'"), ""))' 2>/dev/null || true) -# 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 maintenance and compaction script for OpenClaw. -1. Pre-warms OS page cache for SQLite databases on spinning disks. -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 -import os -import sqlite3 -import subprocess -import sys -import time - -THRESHOLD_TOKENS = 15000 -THRESHOLD_TRANSCRIPT_BYTES = 200000 -THRESHOLD_EVENT_COUNT = 30 -CRON_MAX_AGE_HOURS = 48 -RPC_TIMEOUT_MS = 180000 - -# Tier A: High-frequency interaction and tracking topics (koch, hebamme, shopping, mila_tracker, nutrition_tracker). -# Deep-compaction keeps a compact summary + latest 2 turns without completely wiping conversational memory. -TIER_A_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"), - ("mila_tracker", "agent:mila_tracker:telegram:group:-1004326921510:topic:2796"), - ("nutrition_tracker", "agent:nutrition_tracker:telegram:group:-1004326921510:topic:2797"), -] - -def prewarm_page_cache(): - print("=== Pre-warming SQLite databases into OS page cache ===") - t0 = time.time() - try: - subprocess.run( - "cat /home/node/.openclaw/state/openclaw.sqlite /home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite > /dev/null 2>&1 || true", - shell=True, - timeout=90.0 - ) - print(f"Page cache pre-warmed in {time.time() - t0:.2f}s") - except Exception as e: - print(f"Page cache pre-warming warning: {e}") - -def prune_stale_cron_sessions(): - print(f"=== Pruning isolated cron sessions older than {CRON_MAX_AGE_HOURS}h ===") - cutoff_ms = int(time.time() * 1000) - (CRON_MAX_AGE_HOURS * 3600 * 1000) - total_pruned = 0 - total_tokens_reclaimed = 0 - - for db in sorted(glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")): - agent = db.split("/")[5] - try: - conn = sqlite3.connect(db, timeout=30.0) - conn.execute("PRAGMA foreign_keys = ON;") - cursor = conn.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type=\"table\"") - all_tables = set(r[0] for r in cursor.fetchall()) - - cursor.execute("SELECT session_key, current_session_id, updated_at, entry_json FROM session_nodes WHERE session_key LIKE \"%:cron:%\"") - stale = [] - for k, sid, up, ej in cursor.fetchall(): - if up and up < cutoff_ms: - tok = 0 - if ej: - try: - tok = json.loads(ej).get("totalTokens", 0) or 0 - except Exception: - pass - stale.append((k, sid, tok)) - - if stale: - tokens_reclaimed = sum(t[2] for t in stale) - for k, sid, _ in stale: - for t in ["trajectory_runtime_events", "transcript_events", "transcript_event_identities", "session_transcript_active_events", "transcript_rewrite_watermarks", "session_transcript_index_state", "session_transcript_fts", "session_conversations"]: - if t in all_tables and sid: - cursor.execute(f"DELETE FROM {t} WHERE session_id = ?", (sid,)) - for t in ["session_participants", "heartbeat_outcomes", "session_progress_cards", "board_widgets", "board_tabs"]: - if t in all_tables: - cursor.execute(f"DELETE FROM {t} WHERE session_key = ?", (k,)) - if "memory_entry_origins" in all_tables: - cursor.execute("DELETE FROM memory_entry_origins WHERE session_key = ? OR session_id = ?", (k, sid or "")) - if "session_windows" in all_tables: - cursor.execute("DELETE FROM session_windows WHERE session_key = ? OR session_id = ?", (k, sid or "")) - cursor.execute("DELETE FROM session_nodes WHERE session_key = ?", (k,)) - conn.commit() - total_pruned += len(stale) - total_tokens_reclaimed += tokens_reclaimed - print(f" [{agent}] Pruned {len(stale)} dead cron sessions ({tokens_reclaimed} tokens reclaimed)") - - # Unconditionally resolve any dangling foreign key references across all tables - while True: - cursor.execute("PRAGMA foreign_key_check;") - violations = cursor.fetchall() - if not violations: - break - for table, rowid, parent, fkid in violations: - cursor.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) - conn.commit() - conn.close() - except Exception as e: - print(f" [{agent}] Pruning error: {e}") - - # Clear resolved quarantine entries if all databases are healthy - try: - q_path = "/home/node/.openclaw/state/openclaw-quarantine.sqlite" - if os.path.exists(q_path): - q_conn = sqlite3.connect(q_path) - q_cur = q_conn.cursor() - q_cur.execute("DELETE FROM quarantined_databases") - q_conn.commit() - q_conn.close() - except Exception as e: - print(f"Quarantine cleanup error: {e}") - - print(f"Cron pruning complete: {total_pruned} sessions pruned, {total_tokens_reclaimed} tokens reclaimed.") - -def compact_tier_a_sessions(): - print("=== Tier A: Deep-Compaction (koch, hebamme, shopping) ===") - for agent, key in TIER_A_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 - ev_cnt, ev_bytes = get_session_metrics(agent, key, row[0]) - if ev_cnt <= 2 and ev_bytes < 20000: - print(f" [Tier A] {agent} ({key}) has only {ev_cnt} events ({ev_bytes} bytes), skipping.") - continue - print(f" [Tier A] Deep-compacting {agent} ({key}) [{ev_cnt} events, {ev_bytes//1024}KB]...") - r = subprocess.run( - ["openclaw", "sessions", "compact", key, "--agent", agent, "--timeout", str(RPC_TIMEOUT_MS)], - capture_output=True, text=True - ) - if r.returncode == 0: - print(f" ✓ Deep-compacted: {r.stdout.strip()}") - elif "Already compacted" in r.stderr or "Already compacted" in r.stdout: - print(f" ✓ Already compacted.") - else: - print(f" ✗ Standard compact error: {r.stderr.strip()}; retrying with --max-lines 40...") - fb = subprocess.run( - ["openclaw", "sessions", "compact", key, "--agent", agent, "--max-lines", "40", "--timeout", str(RPC_TIMEOUT_MS)], - capture_output=True, text=True - ) - if fb.returncode == 0: - print(f" ✓ Truncation fallback success: {fb.stdout.strip()}") - else: - print(f" ✗ Truncation fallback failed: {fb.stderr.strip()}") - except Exception as e: - print(f" ✗ Exception deep-compacting {agent}: {e}") - time.sleep(2) - -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): - return 0, 0 - try: - con = sqlite3.connect(db_path, timeout=5.0) - cur = con.cursor() - if not sid: - row = cur.execute("SELECT current_session_id FROM session_nodes WHERE session_key=?", (key,)).fetchone() - sid = row[0] if row else None - if not sid: - con.close() - return 0, 0 - ev_cnt, ev_bytes = cur.execute( - "SELECT count(*), coalesce(sum(length(event_json)), 0) FROM transcript_events WHERE session_id=?", - (sid,) - ).fetchone() - con.close() - return ev_cnt, ev_bytes - except Exception: - return 0, 0 - -def compact_active_sessions(): - tier_a_keys = set(t[1] for t in TIER_A_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}") - return - - sessions = json.loads(res.stdout).get("sessions", []) - compacted = 0 - skipped = 0 - - for s in sessions: - key = s.get("key", "") - agent = s.get("agentId") or "main" - 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 - ":telegram:" in key or - (key.startswith("agent:") and key.endswith(":main")) - ) - if is_target and not key.endswith(":heartbeat"): - ev_cnt, ev_bytes = get_session_metrics(agent, key, sid) - needs_compaction = ( - tokens >= THRESHOLD_TOKENS or - ev_bytes >= THRESHOLD_TRANSCRIPT_BYTES or - (tokens == 0 and ev_cnt >= THRESHOLD_EVENT_COUNT) - ) - if needs_compaction: - stat_str = f"{tokens} tokens" if tokens else f"{ev_bytes//1024}KB / {ev_cnt} events" - print(f"Compacting {agent} ({stat_str}) -> {key}...") - c_res = subprocess.run( - ["openclaw", "sessions", "compact", key, "--agent", agent, "--timeout", str(RPC_TIMEOUT_MS)], - capture_output=True, text=True - ) - if c_res.returncode == 0: - print(f" ✓ {c_res.stdout.strip()}") - compacted += 1 - elif "Already compacted" in c_res.stderr or "Already compacted" in c_res.stdout: - print(f" ✓ Already compacted.") - skipped += 1 - else: - print(f" ✗ {c_res.stderr.strip()}; retrying with --max-lines 80 fallback...") - fb_res = subprocess.run( - ["openclaw", "sessions", "compact", key, "--agent", agent, "--max-lines", "80", "--timeout", str(RPC_TIMEOUT_MS)], - capture_output=True, text=True - ) - if fb_res.returncode == 0: - print(f" ✓ (fallback) {fb_res.stdout.strip()}") - compacted += 1 - else: - print(f" ✗ Fallback failed: {fb_res.stderr.strip()}") - time.sleep(5) # 5s pause to avoid rotational HDD I/O saturation - else: - skipped += 1 - - print(f"Compaction complete: {compacted} compacted, {skipped} skipped.") - -def checkpoint_wal_journals(): - print("=== Checkpointing SQLite WAL journals across all agent databases ===") - dbs = ["/home/node/.openclaw/state/openclaw.sqlite"] + glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite") - wal_freed = 0 - for db in dbs: - wal = db + "-wal" - if os.path.exists(wal): - sz = os.path.getsize(wal) - try: - con = sqlite3.connect(db, timeout=30.0) - con.execute("PRAGMA wal_checkpoint(TRUNCATE);") - con.close() - wal_freed += sz - except Exception as e: - print(f" ✗ Error checkpointing {db}: {e}") - print(f"WAL Checkpoint complete: {wal_freed / (1024*1024):.2f} MB merged to disk.") - -def vacuum_and_optimize(): - 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=30.0) - con.execute("PRAGMA optimize;") - con.close() - except Exception as e: - print(f" ✗ Error optimizing {db}: {e}") - print("SQLite optimization complete.") - -def main(): - prewarm_page_cache() - try: - subprocess.run(["python3", "/home/node/.openclaw/scripts/recover_sessions.py"], check=False) - except Exception: - pass - prune_stale_cron_sessions() - compact_tier_a_sessions() - compact_active_sessions() - checkpoint_wal_journals() - vacuum_and_optimize() - -if __name__ == "__main__": - main() -EOF -chmod +x /home/node/.openclaw/scripts/compact_sessions.py -' - -# 2. Register / Update the OpenClaw cron job -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) - -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" \ +if [ -n "$EXISTING_NIGHTLY_ID" ]; then + echo "Updating existing nightly compaction automation '$EXISTING_NIGHTLY_ID' ($CRON_SCHEDULE @ $TIMEZONE)..." + kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron edit "$EXISTING_NIGHTLY_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)..." + echo "Registering new nightly compaction automation '$NIGHTLY_JOB' ($CRON_SCHEDULE @ $TIMEZONE)..." kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron add \ --name "Nightly Session Compaction" \ - --declaration-key "$JOB_NAME" \ + --declaration-key "$NIGHTLY_JOB" \ --description "Nightly multi-agent session compaction and SQLite WAL checkpointing" \ --cron "$CRON_SCHEDULE" \ --tz "$TIMEZONE" \ @@ -344,7 +82,4 @@ else --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 +echo "=== All session maintenance automations successfully registered/updated! ==="