feat(openclaw): modularize scripts, implement crons-as-code and EaC governance

This commit is contained in:
Moritz Graf
2026-10-16 21:25:33 +02:00
parent f4d8210cc4
commit ed3529a9d7
16 changed files with 1924 additions and 1 deletions
+7
View File
@@ -2,3 +2,10 @@ k8s/gitea-chart/
.vagrant
k8s/tekton-pipelines/auth
.DS_Store
node_modules/
**/node_modules/
dist/
**/dist/
*.tsbuildinfo
.venv/
**/__pycache__/
+16
View File
@@ -60,8 +60,24 @@ To perform modifications or rotate api keys:
./k8s/openclaw/setup-garmin-cron.sh # Nightly Garmin Connect sync (Daily 03:30)
./k8s/openclaw/setup-compaction-cron.sh # Nightly multi-agent session compaction (Daily 04:45)
./k8s/openclaw/setup-itguy-cron.sh # Weekly health audit (Sun 03:00)
# Or run declarative reconciler:
python3 k8s/openclaw/scripts/sync_crons.py
```
### 🛡️ Everything-as-Code (EaC) Governance for New Crons, Skills & Binaries (ADR 0003)
To prevent PVC snowflake drift, **every** new automation, skill, CLI binary, or Python dependency added to OpenClaw MUST be persisted as code:
1. **New Cron Jobs (`Crons-as-Code`)**:
- **System / Recurring Automations**: Never leave a recurring cron job exclusively inside the pod's SQLite database (`openclaw cron add`). Every permanent automation MUST be codified in `k8s/openclaw/crons.yaml` (or the declarative cron sync scripts) and committed to `infrapuzzle`.
- **Dynamic / User-Created Reminders**: Conversational or temporary crons created by agents at runtime must be exported to `knowledge-wiki/operations/crons-backup.json` during nightly backup so they survive PVC loss.
2. **New Skills (`Skills-as-Code`)**:
- Never leave newly created or agent-authored skills only in `/home/node/.openclaw/skills/`, `workspace*/skills/`, or `skill-workshop/` on the PVC.
- All custom skills MUST be committed and pushed to the **`mop-skills`** repository (`git.moritzgraf.de/moritz/mop-skills`) so they are version-controlled and synced cleanly via the `git-sync` sidecar.
3. **New CLI Binaries, Packages & Patches (`Dependencies-as-Code`)**:
- Never manually drop binaries into `/home/node/.openclaw/bin/` or run ad-hoc `uv pip install` / `npm install -g` on the PVC without codifying them.
- Every new binary (e.g. `ffmpeg`, `kubectl`, `jq`, `openhue`, `sonos`), Python package in `mcp-venv`, or runtime patch MUST be added to the declarative bootstrap script (`k8s/openclaw/scripts/bootstrap-deps.sh` / `configmap-scripts.yaml`) so a fresh PVC rebuild has 100% tool parity.
---
### 4. 🗂️ The Three Core Repositories Architecture & Weekly Self-Audit
+940
View File
@@ -0,0 +1,940 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: openclaw-scripts
namespace: openclaw
labels:
app.kubernetes.io/name: openclaw
app.kubernetes.io/part-of: openclaw
data:
bootstrap-deps.sh: "#!/bin/sh\nset -e\n\necho \"=== OpenClaw Dependency & Runtime\
\ Bootstrap ===\"\nexport PATH=\"/home/node/.openclaw/bin:/home/node/.openclaw/.local/bin:$PATH\"\
\n\nSCRIPTS_DIR=\"/home/node/.openclaw/scripts\"\nmkdir -p /home/node/.openclaw/bin\
\ \"$SCRIPTS_DIR\" /home/node/.openclaw/.config\ntouch /home/node/.openclaw/.config/keep\n\
\n# 1. Sanitize legacy config keys in openclaw.json\nnode -e '\n const fs = require(\"\
fs\");\n for (const file of [\"/home/node/.openclaw/openclaw.json\", \"/home/openclaw/.openclaw/openclaw.json\"\
]) {\n if (fs.existsSync(file)) {\n try {\n const cfg = JSON.parse(fs.readFileSync(file,\
\ \"utf8\"));\n if (cfg.browser) {\n delete cfg.browser.remoteCdpTimeoutMs;\n\
\ if (cfg.browser.profiles) {\n if (cfg.browser.profiles.default)\
\ delete cfg.browser.profiles.default.color;\n if (cfg.browser.profiles.chrome)\
\ delete cfg.browser.profiles.chrome;\n }\n }\n if (cfg.gateway\
\ && cfg.gateway.controlUi) {\n delete cfg.gateway.controlUi.dangerouslyDisableDeviceAuth;\n\
\ }\n if (!cfg.agents) cfg.agents = {};\n cfg.agents.ownership\
\ = \"explicit\";\n if (cfg.agents.entries && cfg.agents.entries.main)\
\ {\n delete cfg.agents.entries.main.default;\n }\n fs.writeFileSync(file,\
\ JSON.stringify(cfg, null, 2));\n console.log(\"Sanitized config file:\"\
, file);\n } catch (e) {\n console.error(\"Failed to sanitize\", file,\
\ e.message);\n }\n }\n }\n' || true\n\n# 2. Run pre-flight session recovery\n\
echo \"Running pre-flight self-healing...\"\npython3 \"$SCRIPTS_DIR/recover_sessions.py\"\
\ || true\n\n# 3. Doctor fix\necho \"Running openclaw doctor --fix to migrate\
\ runtime config on PVC...\"\nopenclaw doctor --fix || true\n\n# 4. Garmin OAuth\
\ tokens bootstrap / preserve\nif [ ! -s /home/node/.openclaw/garmin_tokens/garmin_tokens.json\
\ ] && [ -n \"$GARMIN_TOKENS_BASE64\" ]; then\n echo \"Bootstrapping initial\
\ Garmin OAuth tokens from secret...\"\n mkdir -p /home/node/.openclaw/garmin_tokens\n\
\ echo \"$GARMIN_TOKENS_BASE64\" | base64 -d > /home/node/.openclaw/garmin_tokens/garmin_tokens.json\n\
\ chmod 700 /home/node/.openclaw/garmin_tokens\n chmod 600 /home/node/.openclaw/garmin_tokens/garmin_tokens.json\n\
elif [ -s /home/node/.openclaw/garmin_tokens/garmin_tokens.json ]; then\n echo\
\ \"Preserving live Garmin OAuth tokens on PVC...\"\n chmod 700 /home/node/.openclaw/garmin_tokens\n\
\ chmod 600 /home/node/.openclaw/garmin_tokens/garmin_tokens.json\nfi\n\n# 5.\
\ Cleanup legacy binaries and superseded local skills\nrm -rf /home/node/.openclaw/bin/whisper*\
\ /home/node/.openclaw/bin/.whisper* /home/node/.openclaw/scripts/whisper* /home/node/.openclaw/models/whisper\
\ || true\nrm -rf /home/node/.openclaw/skills/memobird /home/node/.openclaw/skills/nerd-report\
\ /home/node/.openclaw/skills/rvv-regensburg /home/node/.openclaw/skills/skill-creator\
\ || true\nrm -rf /home/node/.openclaw/workspace/skills /home/node/.openclaw/workspace-koch/skills\
\ /home/node/.openclaw/workspace-recherche/skills || true\n\n# 6. Ensure tools.allow\
\ and models.providers.openrouter.apiKey unset\nopenclaw config unset tools.allow\
\ || true\nopenclaw config unset models.providers.openrouter.apiKey || true\n\
openclaw config unset agents.entries.mila_tracker.tools.profile || true\nopenclaw\
\ config unset agents.entries.nutrition_tracker.tools.profile || true\n\n# 7.\
\ Decouple hardlinked bootstrap files\nfor dir in /home/node/.openclaw/workspace-*;\
\ do\n if [ -d \"$dir\" ]; then\n for file in USER.md MEMORY.md; do\n \
\ if [ -f \"/home/node/.openclaw/workspace/$file\" ]; then\n cp --remove-destination\
\ \"/home/node/.openclaw/workspace/$file\" \"$dir/$file\"\n chown node:node\
\ \"$dir/$file\" 2>/dev/null || true\n chmod 664 \"$dir/$file\" 2>/dev/null\
\ || true\n fi\n done\n fi\ndone\n\n# 8. Install CLI binaries\nif [ !\
\ -f \"/home/node/.openclaw/bin/openhue.real\" ]; then\n echo \"Installing openhue\
\ CLI binary...\"\n curl -sL https://github.com/openhue/openhue-cli/releases/download/0.24/openhue_Linux_x86_64.tar.gz\
\ | tar -xz -C /home/node/.openclaw/bin openhue\n mv /home/node/.openclaw/bin/openhue\
\ /home/node/.openclaw/bin/openhue.real\n printf '#!/bin/sh\\nexport HOME=/home/node/.openclaw\\\
nexec /home/node/.openclaw/bin/openhue.real \"$@\"\\n' > /home/node/.openclaw/bin/openhue\n\
\ chmod +x /home/node/.openclaw/bin/openhue /home/node/.openclaw/bin/openhue.real\n\
fi\n\nif [ ! -f \"/home/node/.openclaw/bin/summarize\" ]; then\n echo \"Installing\
\ summarize CLI via npm...\"\n npm install -g --prefix /home/node/.openclaw @steipete/summarize\
\ || true\nfi\n\nif [ ! -f \"/home/node/.openclaw/bin/mcp-server-github\" ]; then\n\
\ echo \"Installing @modelcontextprotocol/server-github via npm...\"\n npm install\
\ -g --prefix /home/node/.openclaw @modelcontextprotocol/server-github || true\n\
fi\n\nif [ ! -f \"/home/node/.openclaw/bin/kubectl\" ]; then\n echo \"Installing\
\ kubectl CLI binary...\"\n curl -fsSL https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl\
\ -o /home/node/.openclaw/bin/kubectl\n chmod +x /home/node/.openclaw/bin/kubectl\n\
fi\n\nif [ ! -f \"/home/node/.openclaw/bin/jq\" ]; then\n echo \"Installing jq\
\ CLI binary...\"\n curl -fsSL https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64\
\ -o /home/node/.openclaw/bin/jq\n chmod +x /home/node/.openclaw/bin/jq\nfi\n\
\nif [ ! -f \"/home/node/.openclaw/bin/file\" ]; then\n echo \"Setting up file\
\ CLI helper...\"\n cp \"$SCRIPTS_DIR/file_cli.py\" /home/node/.openclaw/bin/file\n\
\ chmod +x /home/node/.openclaw/bin/file\nfi\n\nif [ -x \"/home/node/.openclaw/.local/bin/python3\"\
\ ]; then\n /home/node/.openclaw/bin/uv pip install --break-system-packages --python\
\ /home/node/.openclaw/.local/bin/python3 pillow google-api-python-client pypdf\
\ || true\nfi\n\n# 9. Clone or pull infrapuzzle repository to PVC\nmkdir -p /home/node/.openclaw/repos\n\
if [ ! -d \"/home/node/.openclaw/repos/infrapuzzle/.git\" ]; then\n echo \"Cloning\
\ infrapuzzle repository to PVC...\"\n git clone --depth 1 \"https://${GIT_SYNC_USERNAME}:${GIT_SYNC_PASSWORD}@git.moritzgraf.de/moritz/infrapuzzle.git\"\
\ /home/node/.openclaw/repos/infrapuzzle || true\nelse\n echo \"Pulling latest\
\ infrapuzzle commits on PVC...\"\n git -C /home/node/.openclaw/repos/infrapuzzle\
\ pull origin master || git -C /home/node/.openclaw/repos/infrapuzzle pull origin\
\ main || true\nfi\n\n# 10. Seed and maintain persistent mcp-venv\nif [ ! -d \"\
/home/node/.openclaw/mcp-venv\" ]; then\n echo \"Seeding persistent mcp-venv...\"\
\n uv venv --python 3.12 /home/node/.openclaw/mcp-venv\n uv pip install --python\
\ /home/node/.openclaw/mcp-venv \\\n prometheus-mcp-server \\\n ha-mcp \\\
\n chuk-mcp-open-meteo \\\n gitea-mcp \\\n git+https://github.com/Taxuspt/garmin_mcp\
\ \\\n \"mcp>=2.0\" \\\n filetype \\\n google-api-python-client \\\n\
\ google-auth \\\n bring-api \\\n requests \\\n beautifulsoup4 \\\n\
\ youtube-transcript-api\nfi\n\nif [ -d \"/home/node/.openclaw/mcp-venv\" ];\
\ then\n python3 -c 'import os; p=\"/home/node/.openclaw/mcp-venv/lib/python3.12/site-packages/mcp/server/fastmcp.py\"\
; open(p, \"w\").write(\"import os\\nfrom fastmcp import FastMCP as _FastMCP,\
\ Context\\n\\nclass FastMCP(_FastMCP):\\n def __init__(self, name=\\\"MCP\
\ Server\\\", host=None, port=None, **kwargs):\\n if host is not None:\
\ os.environ[\\\"FASTMCP_HOST\\\"] = str(host)\\n if port is not None:\
\ os.environ[\\\"FASTMCP_PORT\\\"] = str(port)\\n super().__init__(name=name,\
\ **kwargs)\\n\")' || true\n python3 \"$SCRIPTS_DIR/patch_garmin_mcp.py\" ||\
\ true\nfi\n\n# 11. Extensions & Plugins\nif [ ! -d \"/home/node/.openclaw/extensions/perplexity\"\
\ ]; then\n echo \"Installing perplexity plugin...\"\n openclaw plugins install\
\ clawhub:@openclaw/perplexity-plugin --accept-capabilities || true\nfi\necho\
\ \"Enabling memory-wiki plugin...\"\nopenclaw plugins enable memory-wiki || true\n\
\necho \"Patching perplexity web search runtime for chat completions compatibility...\"\
\nnode \"$SCRIPTS_DIR/patch_perplexity.mjs\" || true\n\n# 12. Ensure OpenRouter\
\ auth profile in state DB\necho \"Ensuring OpenRouter auth profile in state DB...\"\
\nnode --input-type=module -e '\n import { DatabaseSync } from \"node:sqlite\"\
;\n const key = process.env.OPENROUTER_API_KEY;\n if (key) {\n try {\n \
\ const dbPath = \"/home/node/.openclaw/state/openclaw.sqlite\";\n const\
\ db = new DatabaseSync(dbPath);\n db.prepare(\"CREATE TABLE IF NOT EXISTS\
\ config_machine_state (state_key TEXT PRIMARY KEY, value_json TEXT NOT NULL,\
\ updated_at_ms INTEGER NOT NULL)\").run();\n const row = db.prepare(\"SELECT\
\ value_json FROM config_machine_state WHERE state_key = \\x27authProfiles.store\\\
x27\").get();\n let store = row?.value_json ? JSON.parse(row.value_json)\
\ : { version: 1, profiles: {} };\n if (!store.profiles) store.profiles =\
\ {};\n store.profiles[\"openrouter:default\"] = {\n type: \"api_key\"\
,\n provider: \"openrouter\",\n key: key\n };\n db.prepare(\"\
INSERT INTO config_machine_state (state_key, value_json, updated_at_ms) VALUES\
\ (\\x27authProfiles.store\\x27, ?, ?) ON CONFLICT(state_key) DO UPDATE SET value_json\
\ = excluded.value_json, updated_at_ms = excluded.updated_at_ms\")\n .run(JSON.stringify(store),\
\ Date.now());\n console.log(\"Synchronized openrouter auth profile in state\
\ DB.\");\n } catch (e) {\n console.error(\"Could not sync auth profile:\"\
, e.message);\n }\n }\n' || true\n\n# 13. Pre-warm SQLite page cache\necho\
\ \"Pre-warming SQLite database files into OS page cache for instant cold start...\"\
\ncat /home/node/.openclaw/state/openclaw.sqlite /home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite\
\ > /dev/null 2>&1 || true\n\necho \"=== Bootstrap Complete ===\"\n"
gateway-preflight.mjs: " import fs from \"node:fs\";\n import\
\ path from \"node:path\";\n import { execSync, spawn } from \"node:child_process\"\
;\n\n if (!process.env.__OPENCLAW_PREFLIGHT_DONE) {\n process.env.__OPENCLAW_PREFLIGHT_DONE\
\ = \"1\";\n\n const isGateway = process.argv.some(arg => arg.endsWith(\"\
openclaw.mjs\") || arg.endsWith(\"openclaw\")) && process.argv.includes(\"gateway\"\
);\n\n if (isGateway) {\n const t0 = Date.now();\n \
\ console.log(\"[gateway-preflight] Initializing container self-healing\
\ preflight...\");\n\n // 1. Clean up stale gateway lock files from\
\ ungraceful kills\n for (const dir of [\"/tmp/openclaw-1000\", \"\
/tmp\"]) {\n if (fs.existsSync(dir)) {\n try {\n\
\ const files = fs.readdirSync(dir);\n for\
\ (const file of files) {\n if (file.startsWith(\"gateway\"\
) && file.endsWith(\".lock\")) {\n const fullPath = path.join(dir,\
\ file);\n fs.unlinkSync(fullPath);\n \
\ console.log(`[gateway-preflight] Removed stale lock: ${fullPath}`);\n\
\ }\n }\n } catch (e)\
\ {\n console.warn(`[gateway-preflight] Lock cleanup warning\
\ in ${dir}:`, e.message);\n }\n }\n \
\ }\n\n // 2. Clear false database quarantines\n \
\ const qPath = \"/home/node/.openclaw/state/openclaw-quarantine.sqlite\";\n\
\ if (fs.existsSync(qPath)) {\n try {\n \
\ execSync(`/home/node/.openclaw/.local/bin/python3 -c \"import sqlite3;\
\ c=sqlite3.connect(\\x27${qPath}\\x27); c.execute(\\x27DELETE FROM quarantined_databases\\\
x27); c.commit(); c.close()\"`, { stdio: \"ignore\" });\n console.log(\"\
[gateway-preflight] Cleared database quarantine.\");\n } catch\
\ (_) {}\n }\n\n // 3. Deduplicate subagent vector databases\
\ (keeps non-main agent DBs < 15MB)\n try {\n execSync(\"\
/home/node/.openclaw/.local/bin/python3 /home/node/.openclaw/scripts/dedup_subagent_vectors.py\"\
, { stdio: \"inherit\" });\n } catch (e) {\n console.warn(\"\
[gateway-preflight] Vector deduplication warning:\", e.message);\n \
\ }\n\n // 4. Pre-warm page cache sequentially (transforms random\
\ 70 IOPS disk reads into sequential streaming)\n try {\n \
\ const pw0 = Date.now();\n execSync(\"cat /home/node/.openclaw/state/openclaw.sqlite\
\ /home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite > /dev/null 2>&1 ||\
\ true\");\n console.log(`[gateway-preflight] Pre-warmed SQLite\
\ databases in ${Date.now() - pw0}ms`);\n } catch (_) {}\n\n \
\ // 5. Recover any wedged running sessions left over from ungraceful\
\ container restart\n try {\n execSync(\"/home/node/.openclaw/.local/bin/python3\
\ /home/node/.openclaw/scripts/recover_sessions.py\", { stdio: \"inherit\" });\n\
\ } catch (e) {\n console.warn(\"[gateway-preflight]\
\ Session recovery warning:\", e.message);\n }\n\n //\
\ 6. Spawn background turn replay daemon if interrupted turns were detected\n\
\ try {\n if (fs.existsSync(\"/tmp/openclaw-interrupted-turns.json\"\
)) {\n console.log(\"[gateway-preflight] Spawning background\
\ turn replay daemon...\");\n const child = spawn(\"/home/node/.openclaw/.local/bin/python3\"\
, [\"/home/node/.openclaw/scripts/replay_interrupted_turns.py\"], {\n \
\ detached: true,\n stdio: \"ignore\"\n \
\ });\n child.unref();\n }\n \
\ } catch (e) {\n console.warn(\"[gateway-preflight] Could\
\ not spawn turn replay daemon:\", e.message);\n }\n\n \
\ console.log(`[gateway-preflight] Self-healing completed in ${Date.now() -\
\ t0}ms.`);\n }\n }\n"
recover_sessions.py: " #!/usr/bin/env python3\n import glob, sqlite3,\
\ json, os, sys, time\n\n t0 = time.time()\n recovered = 0\n\
\ interrupted_turns = []\n\n def extract_prompt(msg):\n \
\ if not msg: return \"\"\n content = msg.get(\"content\"\
)\n if isinstance(content, str): return content\n if\
\ isinstance(content, list):\n parts = []\n \
\ for c in content:\n if isinstance(c, dict): parts.append(c.get(\"\
text\") or c.get(\"content\") or \"\")\n elif isinstance(c,\
\ str): parts.append(c)\n return \"\\n\".join(filter(None, parts))\n\
\ return str(content or \"\")\n\n for db in sorted(glob.glob(\"\
/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite\")):\n \
\ try:\n conn = sqlite3.connect(db, timeout=10.0)\n \
\ cursor = conn.cursor()\n while True:\n \
\ cursor.execute(\"PRAGMA foreign_key_check;\")\n \
\ violations = cursor.fetchall()\n if not violations: break\n\
\ for table, rowid, parent, fkid in violations:\n \
\ cursor.execute(f\"DELETE FROM {table} WHERE rowid = ?\", (rowid,))\n\
\ cursor.execute(\"SELECT session_key, entry_json, current_session_id\
\ FROM session_nodes WHERE status = \\\"running\\\"\")\n rows\
\ = cursor.fetchall()\n agent_id = os.path.basename(os.path.dirname(os.path.dirname(db)))\n\
\ for sk, ej, csid in rows:\n try:\n \
\ p = json.loads(ej) if ej else {}\n \
\ sid = csid or p.get(\"sessionId\")\n if sid:\n\
\ ev = cursor.execute(\"SELECT seq, event_json FROM\
\ transcript_events WHERE session_id = ? ORDER BY seq DESC LIMIT 1\", (sid,)).fetchone()\n\
\ if ev:\n ev_seq,\
\ ev_raw = ev\n try:\n \
\ ev_data = json.loads(ev_raw)\n \
\ msg = ev_data.get(\"message\") or ev_data\n \
\ if msg.get(\"role\") == \"user\":\n \
\ prompt_text = extract_prompt(msg)\n \
\ ctx = p.get(\"restartRecoveryDeliveryContext\") or p.get(\"delivery\"\
, {}).get(\"context\") or p.get(\"delivery\", {}).get(\"route\", {}).get(\"context\"\
) or {}\n interrupted_turns.append({\n\
\ \"agent\": agent_id,\n \
\ \"session_key\": sk,\n \
\ \"session_id\": sid,\n \
\ \"context\": ctx,\n \
\ \"prompt\": prompt_text\n \
\ })\n cursor.execute(\"PRAGMA foreign_keys\
\ = ON\")\n cursor.execute(\"DELETE FROM\
\ transcript_events WHERE session_id = ? AND seq = ?\", (sid, ev_seq))\n \
\ except Exception as ex:\n \
\ print(f\"[recover_sessions] Error parsing event: {ex}\", file=sys.stderr)\n\
\ p[\"status\"] = \"done\"\n \
\ p[\"activeWriterRunId\"] = None\n p[\"activeClaimRunId\"\
] = None\n p[\"restartRecoveryDeliveryRunId\"] = None\n\
\ cursor.execute(\"UPDATE session_nodes SET status =\
\ \\\"done\\\", entry_json = ? WHERE session_key = ?\", (json.dumps(p), sk))\n\
\ cursor.execute(\"UPDATE session_nodes SET entry_valid\
\ = 1 WHERE session_key = ?\", (sk,))\n recovered +=\
\ 1\n except Exception: pass\n cursor.execute(\"\
UPDATE session_windows SET status = \\\"done\\\" WHERE status = \\\"running\\\"\
\")\n conn.commit()\n conn.close()\n \
\ except Exception as e:\n print(f\"[recover_sessions]\
\ Warning for {db}: {e}\", file=sys.stderr)\n\n if interrupted_turns:\n\
\ try:\n with open(\"/tmp/openclaw-interrupted-turns.json\"\
, \"w\") as f:\n json.dump(interrupted_turns, f, indent=2)\n\
\ print(f\"[recover_sessions] Detected {len(interrupted_turns)}\
\ interrupted user turn(s) saved to /tmp/openclaw-interrupted-turns.json\")\n\
\ except Exception as e:\n print(f\"[recover_sessions]\
\ Error writing interrupted turns: {e}\", file=sys.stderr)\n\n print(f\"\
[recover_sessions] Completed in {time.time() - t0:.2f}s ({recovered} wedged sessions\
\ recovered)\")\n"
dedup_subagent_vectors.py: " #!/usr/bin/env python3\n import glob,\
\ os, sqlite3, sys\n\n vec_path = \"/app/node_modules/sqlite-vec-linux-x64/vec0.so\"\
\n for db in sorted(glob.glob(\"/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite\"\
)):\n agent = os.path.basename(os.path.dirname(os.path.dirname(db)))\n\
\ if agent == \"main\":\n continue\n \
\ try:\n conn = sqlite3.connect(db, timeout=10.0)\n \
\ if os.path.exists(vec_path):\n conn.enable_load_extension(True)\n\
\ conn.load_extension(vec_path)\n cur =\
\ conn.cursor()\n chk = cur.execute(\"SELECT count(*) FROM memory_index_chunks\"\
).fetchone()[0]\n if chk > 0:\n print(f\"\
[dedup_subagents] Purging {chk} duplicated vector chunks from agent {agent}...\"\
)\n for t in [\n \"memory_index_sources\"\
,\n \"memory_index_chunks\",\n \
\ \"memory_index_chunk_recall_metadata\",\n \"memory_index_chunk_provenance\"\
,\n \"memory_entry_origins\",\n \
\ \"memory_embedding_cache\",\n \"memory_index_chunks_vec\"\
\n ]:\n try: cur.execute(f\"DELETE\
\ FROM {t}\")\n except Exception: pass\n \
\ conn.commit()\n cur.execute(\"VACUUM\")\n \
\ print(f\"[dedup_subagents] Reclaimed disk space for agent {agent}\"\
)\n conn.close()\n except Exception as e:\n \
\ print(f\"[dedup_subagents] Notice for {agent}: {e}\", file=sys.stderr)\n"
replay_interrupted_turns.py: " #!/usr/bin/env python3\n import\
\ json, os, subprocess, sys, time, urllib.request\n\n INTERRUPTED_FILE\
\ = \"/tmp/openclaw-interrupted-turns.json\"\n\n if not os.path.exists(INTERRUPTED_FILE):\n\
\ sys.exit(0)\n\n try:\n with open(INTERRUPTED_FILE,\
\ \"r\") as f:\n turns = json.load(f)\n except Exception\
\ as e:\n print(f\"[replay_interrupted_turns] Failed to read {INTERRUPTED_FILE}:\
\ {e}\", file=sys.stderr)\n sys.exit(0)\n\n if not turns:\n\
\ sys.exit(0)\n\n print(f\"[replay_interrupted_turns] Found\
\ {len(turns)} interrupted turn(s). Waiting for gateway /healthz...\")\n\n \
\ gateway_ready = False\n for _ in range(60):\n try:\n\
\ with urllib.request.urlopen(\"http://127.0.0.1:18790/healthz\"\
, timeout=3) as resp:\n if resp.status == 200:\n \
\ gateway_ready = True\n break\n \
\ except Exception:\n pass\n time.sleep(2)\n\
\n if not gateway_ready:\n print(\"[replay_interrupted_turns]\
\ Gateway did not become healthy in 120s. Aborting.\", file=sys.stderr)\n \
\ sys.exit(1)\n\n # Allow 5s for Telegram ingress to bind\n\
\ time.sleep(5)\n\n for turn in turns:\n sk = turn.get(\"\
session_key\")\n sid = turn.get(\"session_id\")\n agent_id\
\ = turn.get(\"agent\")\n ctx = turn.get(\"context\") or {}\n \
\ prompt = turn.get(\"prompt\") or \"\"\n \n \
\ channel = ctx.get(\"channel\") or \"telegram\"\n to_target =\
\ ctx.get(\"to\") or \"\"\n thread_id = ctx.get(\"threadId\")\n \
\ \n target = to_target\n if target.startswith(\"\
telegram:\"):\n parts = target.split(\":\")\n \
\ target = parts[1] if len(parts) > 1 else target\n\n print(f\"\
[replay_interrupted_turns] Processing agent '{agent_id}' on session '{sk}' target='{target}'\
\ thread='{thread_id}'\")\n \n # 1. Send status notification\
\ to Telegram if target exists\n if target:\n notice\
\ = \"\u26A0\uFE0F OpenClaw wurde w\xE4hrend der Bearbeitung neu gestartet \u2013\
\ deine Anfrage wird jetzt nachgeholt...\"\n notify_cmd = [\n\
\ \"openclaw\", \"message\", \"send\",\n \
\ \"--channel\", channel,\n \"--target\", str(target),\n\
\ \"-m\", notice\n ]\n \
\ if thread_id is not None:\n notify_cmd.extend([\"--thread-id\"\
, str(thread_id)])\n try:\n subprocess.run(notify_cmd,\
\ check=False, timeout=30)\n print(f\"[replay_interrupted_turns]\
\ Sent recovery notice to {target}\")\n except Exception as e:\n\
\ print(f\"[replay_interrupted_turns] Notice failed: {e}\"\
, file=sys.stderr)\n\n # 2. Replay agent turn\n if sk\
\ and prompt:\n p_short = (prompt[:60] + \"...\") if len(prompt)\
\ > 60 else prompt\n print(f\"[replay_interrupted_turns] Replaying\
\ agent turn: {p_short}\")\n agent_cmd = [\n \
\ \"openclaw\", \"agent\",\n \"--session-key\", sk,\n\
\ \"-m\", prompt,\n \"--deliver\"\n\
\ ]\n try:\n res = subprocess.run(agent_cmd,\
\ capture_output=True, text=True, timeout=300)\n if res.returncode\
\ == 0:\n print(f\"[replay_interrupted_turns] Turn replay\
\ succeeded for {sk}\")\n else:\n \
\ print(f\"[replay_interrupted_turns] Turn replay exited with code {res.returncode}:\
\ {res.stderr[:200]}\", file=sys.stderr)\n except Exception as\
\ e:\n print(f\"[replay_interrupted_turns] Replay execution\
\ error: {e}\", file=sys.stderr)\n\n try:\n os.remove(INTERRUPTED_FILE)\n\
\ print(f\"[replay_interrupted_turns] Cleaned up {INTERRUPTED_FILE}\"\
)\n except Exception:\n pass\n"
patch_garmin_mcp.py: "import os\n\np = \"/home/node/.openclaw/mcp-venv/lib/python3.12/site-packages/garmin_mcp/workouts.py\"\
\nif os.path.exists(p):\n with open(p, \"r\") as f:\n c = f.read()\n\
\ if \"_normalize_sport_type\" not in c:\n patch = '''\nSPORT_TYPE_MAPPINGS\
\ = {\n \"running\": {\"sportTypeId\": 1, \"sportTypeKey\": \"running\", \"\
displayOrder\": 1},\n \"cycling\": {\"sportTypeId\": 2, \"sportTypeKey\": \"\
cycling\", \"displayOrder\": 2},\n \"other\": {\"sportTypeId\": 3, \"sportTypeKey\"\
: \"other\", \"displayOrder\": 3},\n \"lap_swimming\": {\"sportTypeId\": 4,\
\ \"sportTypeKey\": \"lap_swimming\", \"displayOrder\": 4},\n \"swimming\"\
: {\"sportTypeId\": 4, \"sportTypeKey\": \"lap_swimming\", \"displayOrder\": 4},\n\
\ \"strength_training\": {\"sportTypeId\": 5, \"sportTypeKey\": \"strength_training\"\
, \"displayOrder\": 5},\n \"cardio_training\": {\"sportTypeId\": 6, \"sportTypeKey\"\
: \"cardio_training\", \"displayOrder\": 6},\n \"cardio\": {\"sportTypeId\"\
: 6, \"sportTypeKey\": \"cardio_training\", \"displayOrder\": 6},\n \"yoga\"\
: {\"sportTypeId\": 7, \"sportTypeKey\": \"yoga\", \"displayOrder\": 8},\n \
\ \"pilates\": {\"sportTypeId\": 8, \"sportTypeKey\": \"pilates\", \"displayOrder\"\
: 9},\n \"hiit\": {\"sportTypeId\": 9, \"sportTypeKey\": \"hiit\", \"displayOrder\"\
: 7},\n \"mobility\": {\"sportTypeId\": 11, \"sportTypeKey\": \"mobility\"\
, \"displayOrder\": 11},\n \"walking\": {\"sportTypeId\": 12, \"sportTypeKey\"\
: \"walking\", \"displayOrder\": 10},\n \"rucking\": {\"sportTypeId\": 13,\
\ \"sportTypeKey\": \"rucking\", \"displayOrder\": 12},\n}\nSPORT_ID_MAPPINGS\
\ = {v[\"sportTypeId\"]: v for v in SPORT_TYPE_MAPPINGS.values()}\n\ndef _resolve_sport_block(st):\n\
\ if isinstance(st, str):\n k = st.strip().lower()\n if k in\
\ SPORT_TYPE_MAPPINGS:\n return dict(SPORT_TYPE_MAPPINGS[k])\n elif\
\ isinstance(st, dict):\n key = st.get(\"sportTypeKey\")\n sid =\
\ st.get(\"sportTypeId\")\n if key and str(key).strip().lower() in SPORT_TYPE_MAPPINGS:\n\
\ base = dict(SPORT_TYPE_MAPPINGS[str(key).strip().lower()])\n \
\ if sid is not None and sid != 0 and sid != base[\"sportTypeId\"]:\n \
\ raise ValueError(f\"sportType mismatch: {key} vs {sid}\")\n \
\ return base\n if sid is not None and sid in SPORT_ID_MAPPINGS:\n\
\ return dict(SPORT_ID_MAPPINGS[sid])\n return None\n\ndef _normalize_sport_type(workout_data:\
\ dict) -> None:\n resolved = _resolve_sport_block(workout_data.get(\"sportType\"\
))\n segments = workout_data.get(\"workoutSegments\") or []\n if not resolved\
\ and segments:\n for seg in segments:\n resolved = _resolve_sport_block(seg.get(\"\
sportType\"))\n if resolved:\n break\n if not resolved:\n\
\ raise ValueError(\"Missing or invalid sportType. Both sportTypeId and\
\ sportTypeKey are required.\")\n workout_data[\"sportType\"] = resolved\n\
\ for seg in segments:\n st = _resolve_sport_block(seg.get(\"sportType\"\
))\n seg[\"sportType\"] = st if st else dict(resolved)\n\n'''\n \
\ t = \"def _normalize_workout_steps(workout_data: dict) -> None:\"\n if\
\ t in c:\n with open(p, \"w\") as f:\n f.write(c.replace(t,\
\ patch + t + \"\\n _normalize_sport_type(workout_data)\", 1))\n \
\ print(\"Successfully patched garmin_mcp workouts.py!\")\n"
patch_perplexity.mjs: "import fs from \"node:fs\";\nimport path from \"node:path\"\
;\nimport { globSync } from \"node:fs\";\n\nconst baseDir = \"/home/node/.openclaw/extensions/perplexity/dist\"\
;\nif (fs.existsSync(baseDir)) {\n const files = fs.readdirSync(baseDir).filter(f\
\ => f.startsWith(\"perplexity-web-search-provider.runtime-\") && f.endsWith(\"\
.js\"));\n for (const filename of files) {\n const file = path.join(baseDir,\
\ filename);\n let code = fs.readFileSync(file, \"utf8\");\n if (!code.includes(\"\
/* PATCHED_CHAT_COMPLETIONS */\")) {\n const target = \"const structured\
\ = runtime.transport === \\\"search_api\\\";\";\n const fastPath = `const\
\ structured = runtime.transport === \"search_api\"; /* PATCHED_CHAT_COMPLETIONS\
\ */\n if (!structured) {\n const cacheKey = buildSearchCacheKey([\n\
\ \"perplexity\",\n runtime.transport,\n runtime.baseUrl,\n\
\ runtime.model,\n query,\n freshness\n \
\ ]);\n const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);\n\
\ const cached = readCachedSearchPayload(cacheKey, cacheTtlMs);\n \
\ if (cached) return cached;\n const start = Date.now();\n \
\ const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);\n \
\ const result = await runPerplexitySearch({\n query,\n \
\ apiKey: runtime.apiKey,\n baseUrl: runtime.baseUrl,\n \
\ model: runtime.model,\n timeoutSeconds,\n signal,\n\
\ freshness\n });\n const payload = {\n \
\ query,\n provider: \"perplexity\",\n model: runtime.model,\n\
\ tookMs: Date.now() - start,\n externalContent: {\n \
\ untrusted: true,\n source: \"web_search\",\n \
\ provider: \"perplexity\",\n wrapped: true\n },\n\
\ content: wrapWebContent(result.content, \"web_search\"),\n \
\ citations: result.citations\n };\n writeCachedSearchPayload(cacheKey,\
\ payload);\n return payload;\n }`;\n if (code.includes(target))\
\ {\n code = code.replace(target, fastPath);\n fs.writeFileSync(file,\
\ code, \"utf8\");\n console.log(\"Patched perplexity runtime for chat\
\ completions:\", file);\n }\n }\n }\n}\n"
file_cli.py: "#!/home/node/.openclaw/mcp-venv/bin/python3\nimport sys, os, mimetypes,\
\ filetype\n\ndef get_file_info(filepath, mime_only=False):\n if not os.path.exists(filepath):\n\
\ return \"cannot open (No such file or directory)\", \"cannot open (No\
\ such file or directory)\"\n if os.path.isdir(filepath):\n return \"\
directory\", \"inode/directory\"\n kind = filetype.guess(filepath)\n if\
\ kind is not None:\n return f\"{kind.extension.upper()} image/media data\
\ ({kind.mime})\", kind.mime\n guessed_mime, _ = mimetypes.guess_type(filepath)\n\
\ try:\n with open(filepath, \"rb\") as f:\n sample = f.read(4096)\n\
\ if len(sample) == 0:\n return \"empty\", \"application/x-empty\"\
\n try:\n sample.decode(\"utf-8\")\n if guessed_mime:\n\
\ return f\"{guessed_mime} text\", guessed_mime\n return\
\ \"ASCII text\", \"text/plain\"\n except UnicodeDecodeError:\n \
\ return \"data\", \"application/octet-stream\"\n except Exception as e:\n\
\ return f\"cannot read ({e})\", \"application/octet-stream\"\n\ndef main():\n\
\ args = sys.argv[1:]\n brief, mime_only, files = False, False, []\n \
\ for arg in args:\n if arg in (\"-b\", \"--brief\"):\n brief\
\ = True\n elif arg in (\"-i\", \"--mime\", \"--mime-type\"):\n \
\ mime_only = True\n elif arg.startswith(\"-\") and len(arg) > 1 and\
\ not arg.startswith(\"--\"):\n if \"b\" in arg: brief = True\n \
\ if \"i\" in arg: mime_only = True\n else:\n files.append(arg)\n\
\ if not files:\n sys.stderr.write(\"Usage: file [-b] [-i|--mime-type]\
\ <file>...\\n\")\n sys.exit(1)\n for f in files:\n desc, mime\
\ = get_file_info(f, mime_only)\n res = mime if mime_only else desc\n \
\ print(res if brief else f\"{f}: {res}\")\n\nif __name__ == \"__main__\"\
:\n main()\n"
compact_sessions.py: "#!/usr/bin/env python3\n\"\"\"\nNightly multi-agent session\
\ maintenance and compaction script for OpenClaw.\n1. Pre-warms OS page cache\
\ for SQLite databases on spinning disks.\n2. Recovers any wedged or locked sessions\
\ via recover_sessions.py.\n3. Prunes abandoned isolated cron sessions (>48h old)\
\ to prevent token/state bloat.\n4. Tier A Hard-Reset: Resets koch, hebamme, and\
\ shopping topics to 0 tokens (clean archive).\n5. Tier B Smart-Compaction: Compacts\
\ active sessions exceeding token thresholds (5s pacing).\n6. Checkpoints SQLite\
\ WAL journals across all agent databases to disk.\n7. Optimizes SQLite query\
\ planner without blocking full VACUUM.\n\"\"\"\nimport glob\nimport json\nimport\
\ os\nimport sqlite3\nimport subprocess\nimport sys\nimport time\n\nTHRESHOLD_TOKENS\
\ = 15000\nTHRESHOLD_TRANSCRIPT_BYTES = 200000\nTHRESHOLD_EVENT_COUNT = 30\nCRON_MAX_AGE_HOURS\
\ = 48\nRPC_TIMEOUT_MS = 180000\n\n# Tracking Channels: Deterministic sliding-window\
\ (mila_tracker, nutrition_tracker).\n# Strict sliding window of ~5-8 events (2-3\
\ turns max), 0 LLM tokens, instant truncation.\nTRACKING_TARGETS = [\n (\"\
mila_tracker\", \"agent:mila_tracker:telegram:group:-1004326921510:topic:2796\"\
),\n (\"nutrition_tracker\", \"agent:nutrition_tracker:telegram:group:-1004326921510:topic:2797\"\
),\n]\nMAX_TRACKING_EVENTS = 8\nMAX_TRACKING_LINES = 8\n\n# Tier A: High-frequency\
\ interaction topics (koch, hebamme, shopping).\n# Deep-compaction keeps a compact\
\ summary + latest 2 turns without completely wiping conversational memory.\n\
TIER_A_TARGETS = [\n (\"koch\", \"agent:koch:telegram:group:-1004326921510:topic:21\"\
),\n (\"hebamme\", \"agent:hebamme:telegram:group:-1004326921510:topic:22\"\
),\n (\"shopping\", \"agent:shopping:telegram:group:-1004326921510:topic:115\"\
),\n]\n\ndef prewarm_page_cache():\n print(\"=== Pre-warming SQLite databases\
\ into OS page cache ===\")\n t0 = time.time()\n try:\n subprocess.run(\n\
\ \"cat /home/node/.openclaw/state/openclaw.sqlite /home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite\
\ > /dev/null 2>&1 || true\",\n shell=True,\n timeout=90.0\n\
\ )\n print(f\"Page cache pre-warmed in {time.time() - t0:.2f}s\"\
)\n except Exception as e:\n print(f\"Page cache pre-warming warning:\
\ {e}\")\n\ndef prune_stale_cron_sessions():\n print(f\"=== Pruning isolated\
\ cron sessions older than {CRON_MAX_AGE_HOURS}h ===\")\n cutoff_ms = int(time.time()\
\ * 1000) - (CRON_MAX_AGE_HOURS * 3600 * 1000)\n total_pruned = 0\n total_tokens_reclaimed\
\ = 0\n\n for db in sorted(glob.glob(\"/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite\"\
)):\n agent = db.split(\"/\")[5]\n try:\n conn = sqlite3.connect(db,\
\ timeout=30.0)\n conn.execute(\"PRAGMA foreign_keys = ON;\")\n \
\ cursor = conn.cursor()\n cursor.execute(\"SELECT name FROM\
\ sqlite_master WHERE type=\\\"table\\\"\")\n all_tables = set(r[0]\
\ for r in cursor.fetchall())\n\n cursor.execute(\"SELECT session_key,\
\ current_session_id, updated_at, entry_json FROM session_nodes WHERE session_key\
\ LIKE \\\"%:cron:%\\\"\")\n stale = []\n for k, sid, up,\
\ ej in cursor.fetchall():\n if up and up < cutoff_ms:\n \
\ tok = 0\n if ej:\n try:\n\
\ tok = json.loads(ej).get(\"totalTokens\", 0) or 0\n\
\ except Exception:\n pass\n\
\ stale.append((k, sid, tok))\n\n if stale:\n \
\ tokens_reclaimed = sum(t[2] for t in stale)\n for\
\ k, sid, _ in stale:\n 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\"]:\n if t in all_tables and\
\ sid:\n cursor.execute(f\"DELETE FROM {t} WHERE session_id\
\ = ?\", (sid,))\n for t in [\"session_participants\", \"heartbeat_outcomes\"\
, \"session_progress_cards\", \"board_widgets\", \"board_tabs\"]:\n \
\ if t in all_tables:\n cursor.execute(f\"\
DELETE FROM {t} WHERE session_key = ?\", (k,))\n if \"memory_entry_origins\"\
\ in all_tables:\n cursor.execute(\"DELETE FROM memory_entry_origins\
\ WHERE session_key = ? OR session_id = ?\", (k, sid or \"\"))\n \
\ if \"session_windows\" in all_tables:\n cursor.execute(\"\
DELETE FROM session_windows WHERE session_key = ? OR session_id = ?\", (k, sid\
\ or \"\"))\n cursor.execute(\"DELETE FROM session_nodes WHERE\
\ session_key = ?\", (k,))\n conn.commit()\n total_pruned\
\ += len(stale)\n total_tokens_reclaimed += tokens_reclaimed\n\
\ print(f\" [{agent}] Pruned {len(stale)} dead cron sessions ({tokens_reclaimed}\
\ tokens reclaimed)\")\n\n # Unconditionally resolve any dangling foreign\
\ key references across all tables\n while True:\n cursor.execute(\"\
PRAGMA foreign_key_check;\")\n violations = cursor.fetchall()\n\
\ if not violations:\n break\n \
\ for table, rowid, parent, fkid in violations:\n cursor.execute(f\"\
DELETE FROM {table} WHERE rowid = ?\", (rowid,))\n conn.commit()\n\
\ conn.close()\n except Exception as e:\n print(f\"\
\ [{agent}] Pruning error: {e}\")\n\n # Clear resolved quarantine entries\
\ if all databases are healthy\n try:\n q_path = \"/home/node/.openclaw/state/openclaw-quarantine.sqlite\"\
\n if os.path.exists(q_path):\n q_conn = sqlite3.connect(q_path)\n\
\ q_cur = q_conn.cursor()\n q_cur.execute(\"DELETE FROM\
\ quarantined_databases\")\n q_conn.commit()\n q_conn.close()\n\
\ except Exception as e:\n print(f\"Quarantine cleanup error: {e}\"\
)\n\n print(f\"Cron pruning complete: {total_pruned} sessions pruned, {total_tokens_reclaimed}\
\ tokens reclaimed.\")\n\ndef compact_tracking_sessions():\n print(\"=== Tracking\
\ Channels Sliding-Window Maintenance (mila_tracker, nutrition_tracker) ===\"\
)\n for agent, key in TRACKING_TARGETS:\n db_path = f\"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite\"\
\n if not os.path.exists(db_path):\n continue\n try:\n\
\ con = sqlite3.connect(db_path, timeout=5.0)\n row = con.execute(\"\
SELECT current_session_id FROM session_nodes WHERE session_key=?\", (key,)).fetchone()\n\
\ if not row or not row[0]:\n con.close()\n \
\ print(f\" [{agent}] No active session node found for {key}.\")\n \
\ continue\n sid = row[0]\n cnt = con.execute(\"\
SELECT count(*) FROM transcript_events WHERE session_id=?\", (sid,)).fetchone()[0]\n\
\ con.close()\n\n if cnt > MAX_TRACKING_EVENTS:\n \
\ print(f\" [{agent}] Session {key} has {cnt} events (> {MAX_TRACKING_EVENTS}).\
\ Truncating to last {MAX_TRACKING_LINES} lines...\")\n cmd = [\n\
\ \"openclaw\", \"sessions\", \"compact\", key,\n \
\ \"--agent\", agent,\n \"--max-lines\", str(MAX_TRACKING_LINES),\n\
\ \"--timeout\", \"30000\"\n ]\n \
\ r = subprocess.run(cmd, capture_output=True, text=True)\n \
\ if r.returncode == 0:\n print(f\" \u2713 Truncated successfully:\
\ {r.stdout.strip()}\")\n else:\n print(f\"\
\ \u2717 Truncation failed: {r.stderr.strip()}\")\n else:\n \
\ print(f\" [{agent}] Session {key} has {cnt} events (<= {MAX_TRACKING_EVENTS}).\
\ Clean.\")\n except Exception as e:\n print(f\" \u2717 [{agent}]\
\ Error during tracking compaction: {e}\")\n\ndef compact_tier_a_sessions():\n\
\ print(\"=== Tier A: Deep-Compaction (koch, hebamme, shopping) ===\")\n \
\ for agent, key in TIER_A_TARGETS:\n db_path = f\"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite\"\
\n if not os.path.exists(db_path):\n continue\n try:\n\
\ con = sqlite3.connect(db_path, timeout=10.0)\n row = con.execute(\"\
SELECT current_session_id FROM session_nodes WHERE session_key=?\", (key,)).fetchone()\n\
\ con.close()\n if not row or not row[0]:\n \
\ print(f\" [Tier A] {agent} ({key}) already at 0 (no active session).\")\n\
\ continue\n ev_cnt, ev_bytes = get_session_metrics(agent,\
\ key, row[0])\n if ev_cnt <= 2 and ev_bytes < 20000:\n \
\ print(f\" [Tier A] {agent} ({key}) has only {ev_cnt} events ({ev_bytes}\
\ bytes), skipping.\")\n continue\n print(f\" [Tier\
\ A] Deep-compacting {agent} ({key}) [{ev_cnt} events, {ev_bytes//1024}KB]...\"\
)\n r = subprocess.run(\n [\"openclaw\", \"sessions\"\
, \"compact\", key, \"--agent\", agent, \"--timeout\", str(RPC_TIMEOUT_MS)],\n\
\ capture_output=True, text=True\n )\n if\
\ r.returncode == 0:\n print(f\" \u2713 Deep-compacted: {r.stdout.strip()}\"\
)\n elif \"Already compacted\" in r.stderr or \"Already compacted\"\
\ in r.stdout:\n print(f\" \u2713 Already compacted.\")\n \
\ else:\n print(f\" \u2717 Standard compact error:\
\ {r.stderr.strip()}; retrying with --max-lines 40...\")\n fb =\
\ subprocess.run(\n [\"openclaw\", \"sessions\", \"compact\"\
, key, \"--agent\", agent, \"--max-lines\", \"40\", \"--timeout\", str(RPC_TIMEOUT_MS)],\n\
\ capture_output=True, text=True\n )\n \
\ if fb.returncode == 0:\n print(f\" \u2713 Truncation\
\ fallback success: {fb.stdout.strip()}\")\n else:\n \
\ print(f\" \u2717 Truncation fallback failed: {fb.stderr.strip()}\"\
)\n except Exception as e:\n print(f\" \u2717 Exception deep-compacting\
\ {agent}: {e}\")\n time.sleep(2)\n\ndef get_session_metrics(agent, key,\
\ sid=None):\n db_path = f\"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite\"\
\n if not os.path.exists(db_path):\n return 0, 0\n try:\n \
\ con = sqlite3.connect(db_path, timeout=5.0)\n cur = con.cursor()\n \
\ if not sid:\n row = cur.execute(\"SELECT current_session_id\
\ FROM session_nodes WHERE session_key=?\", (key,)).fetchone()\n sid\
\ = row[0] if row else None\n if not sid:\n con.close()\n \
\ return 0, 0\n ev_cnt, ev_bytes = cur.execute(\n \"\
SELECT count(*), coalesce(sum(length(event_json)), 0) FROM transcript_events WHERE\
\ session_id=?\", \n (sid,)\n ).fetchone()\n con.close()\n\
\ return ev_cnt, ev_bytes\n except Exception:\n return 0, 0\n\
\ndef compact_active_sessions():\n excluded_keys = set(t[1] for t in TIER_A_TARGETS)\
\ | set(t[1] for t in TRACKING_TARGETS)\n print(f\"=== Tier B: Running Multi-Agent\
\ Compaction (threshold: {THRESHOLD_TOKENS} tokens / {THRESHOLD_TRANSCRIPT_BYTES//1024}KB,\
\ LLM summarization) ===\")\n res = subprocess.run([\"openclaw\", \"sessions\"\
, \"--all-agents\", \"--json\"], capture_output=True, text=True)\n if res.returncode\
\ != 0:\n print(f\"Failed to list sessions: {res.stderr}\")\n return\n\
\n sessions = json.loads(res.stdout).get(\"sessions\", [])\n compacted =\
\ 0\n skipped = 0\n\n for s in sessions:\n key = s.get(\"key\", \"\
\")\n agent = s.get(\"agentId\") or \"main\"\n tokens = s.get(\"\
totalTokens\") or 0\n sid = s.get(\"sessionId\")\n\n # Skip Tier\
\ A and Tracking agents as they have dedicated routines\n if key in excluded_keys:\n\
\ continue\n\n # Target main sessions and all telegram sessions\
\ (topics and direct)\n is_target = (\n key == \"agent:main:main\"\
\ or \n \":telegram:\" in key or\n (key.startswith(\"agent:\"\
) and key.endswith(\":main\"))\n )\n if is_target and not key.endswith(\"\
:heartbeat\"):\n ev_cnt, ev_bytes = get_session_metrics(agent, key,\
\ sid)\n needs_compaction = (\n tokens >= THRESHOLD_TOKENS\
\ or\n ev_bytes >= THRESHOLD_TRANSCRIPT_BYTES or\n \
\ (tokens == 0 and ev_cnt >= THRESHOLD_EVENT_COUNT)\n )\n \
\ if needs_compaction:\n stat_str = f\"{tokens} tokens\" if\
\ tokens else f\"{ev_bytes//1024}KB / {ev_cnt} events\"\n print(f\"\
Compacting {agent} ({stat_str}) -> {key}...\")\n c_res = subprocess.run(\n\
\ [\"openclaw\", \"sessions\", \"compact\", key, \"--agent\"\
, agent, \"--timeout\", str(RPC_TIMEOUT_MS)],\n capture_output=True,\
\ text=True\n )\n if c_res.returncode == 0:\n \
\ print(f\" \u2713 {c_res.stdout.strip()}\")\n \
\ compacted += 1\n elif \"Already compacted\" in c_res.stderr\
\ or \"Already compacted\" in c_res.stdout:\n print(f\" \u2713\
\ Already compacted.\")\n skipped += 1\n else:\n\
\ print(f\" \u2717 {c_res.stderr.strip()}; retrying with --max-lines\
\ 80 fallback...\")\n fb_res = subprocess.run(\n \
\ [\"openclaw\", \"sessions\", \"compact\", key, \"--agent\", agent,\
\ \"--max-lines\", \"80\", \"--timeout\", str(RPC_TIMEOUT_MS)],\n \
\ capture_output=True, text=True\n )\n \
\ if fb_res.returncode == 0:\n print(f\" \u2713\
\ (fallback) {fb_res.stdout.strip()}\")\n compacted +=\
\ 1\n else:\n print(f\" \u2717 Fallback\
\ failed: {fb_res.stderr.strip()}\")\n time.sleep(5) # 5s pause\
\ to avoid rotational HDD I/O saturation\n else:\n skipped\
\ += 1\n\n print(f\"Compaction complete: {compacted} compacted, {skipped} skipped.\"\
)\n\ndef checkpoint_wal_journals():\n print(\"=== Checkpointing SQLite WAL\
\ journals across all agent databases ===\")\n dbs = [\"/home/node/.openclaw/state/openclaw.sqlite\"\
] + glob.glob(\"/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite\")\n\
\ wal_freed = 0\n for db in dbs:\n wal = db + \"-wal\"\n if\
\ os.path.exists(wal):\n sz = os.path.getsize(wal)\n try:\n\
\ con = sqlite3.connect(db, timeout=30.0)\n con.execute(\"\
PRAGMA wal_checkpoint(TRUNCATE);\")\n con.close()\n \
\ wal_freed += sz\n except Exception as e:\n print(f\"\
\ \u2717 Error checkpointing {db}: {e}\")\n print(f\"WAL Checkpoint complete:\
\ {wal_freed / (1024*1024):.2f} MB merged to disk.\")\n\ndef vacuum_and_optimize():\n\
\ print(\"=== Running SQLite OPTIMIZE across all databases ===\")\n dbs\
\ = [\"/home/node/.openclaw/state/openclaw.sqlite\"] + glob.glob(\"/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite\"\
)\n for db in dbs:\n try:\n con = sqlite3.connect(db, timeout=30.0)\n\
\ con.execute(\"PRAGMA optimize;\")\n con.close()\n \
\ except Exception as e:\n print(f\" \u2717 Error optimizing {db}:\
\ {e}\")\n print(\"SQLite optimization complete.\")\n\ndef main():\n prewarm_page_cache()\n\
\ try:\n subprocess.run([\"python3\", \"/home/node/.openclaw/scripts/recover_sessions.py\"\
], check=False)\n except Exception:\n pass\n prune_stale_cron_sessions()\n\
\ compact_tracking_sessions()\n compact_tier_a_sessions()\n compact_active_sessions()\n\
\ checkpoint_wal_journals()\n vacuum_and_optimize()\n\nif __name__ == \"\
__main__\":\n main()\n"
compact_tracking_sessions.py: "#!/usr/bin/env python3\n\"\"\"\nLightweight sliding-window\
\ compaction for OpenClaw tracking channels:\n- mila_tracker (topic 2796)\n- nutrition_tracker\
\ (topic 2797)\n\nMaintains a strict sliding window of ~5-8 transcript events\
\ (max 2-3 turns).\nUses native gateway truncation (--max-lines 8) requiring 0\
\ LLM tokens and 0 external API calls.\n\"\"\"\nimport os\nimport sqlite3\nimport\
\ subprocess\nimport sys\nimport time\n\nMAX_TRACKING_EVENTS = 8\nMAX_TRACKING_LINES\
\ = 8\nTIMEOUT_MS = 30000\n\nTRACKING_TARGETS = [\n (\"mila_tracker\", \"agent:mila_tracker:telegram:group:-1004326921510:topic:2796\"\
),\n (\"nutrition_tracker\", \"agent:nutrition_tracker:telegram:group:-1004326921510:topic:2797\"\
),\n]\n\ndef check_and_compact_tracking():\n compacted_count = 0\n skipped_count\
\ = 0\n\n for agent, key in TRACKING_TARGETS:\n db_path = f\"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite\"\
\n if not os.path.exists(db_path):\n continue\n try:\n\
\ con = sqlite3.connect(db_path, timeout=5.0)\n row = con.execute(\"\
SELECT current_session_id FROM session_nodes WHERE session_key=?\", (key,)).fetchone()\n\
\ if not row or not row[0]:\n con.close()\n \
\ continue\n sid = row[0]\n cnt = con.execute(\"\
SELECT count(*) FROM transcript_events WHERE session_id=?\", (sid,)).fetchone()[0]\n\
\ con.close()\n\n if cnt > MAX_TRACKING_EVENTS:\n \
\ print(f\"[{agent}] Session {key} has {cnt} events (threshold {MAX_TRACKING_EVENTS}).\
\ Truncating to last {MAX_TRACKING_LINES} lines...\")\n cmd = [\n\
\ \"openclaw\", \"sessions\", \"compact\", key,\n \
\ \"--agent\", agent,\n \"--max-lines\", str(MAX_TRACKING_LINES),\n\
\ \"--timeout\", str(TIMEOUT_MS)\n ]\n \
\ res = subprocess.run(cmd, capture_output=True, text=True)\n \
\ if res.returncode == 0:\n print(f\" \u2713 [{agent}]\
\ Truncated successfully: {res.stdout.strip()}\")\n compacted_count\
\ += 1\n else:\n print(f\" \u2717 [{agent}]\
\ Truncation failed: {res.stderr.strip()}\", file=sys.stderr)\n else:\n\
\ skipped_count += 1\n except Exception as e:\n \
\ print(f\"[{agent}] Error checking session: {e}\", file=sys.stderr)\n\n \
\ if compacted_count > 0:\n print(f\"Tracking maintenance: {compacted_count}\
\ sessions truncated, {skipped_count} within window.\")\n\nif __name__ == \"__main__\"\
:\n check_and_compact_tracking()\n"
sync_crons.py: "#!/usr/bin/env python3\n\"\"\"\nsync_crons.py\nIdempotently reconciles\
\ declarative OpenClaw automations defined in crons.yaml\nwith the running OpenClaw\
\ gateway state.\n\nCan run directly inside the container or remotely via kubectl\
\ exec.\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport sys\nimport\
\ yaml\n\ndef find_crons_file():\n candidates = [\n os.environ.get(\"\
CRONS_FILE\"),\n \"crons.yaml\",\n \"k8s/openclaw/crons.yaml\",\n\
\ \"/home/node/.openclaw/scripts/crons.yaml\",\n os.path.join(os.path.dirname(__file__),\
\ \"../crons.yaml\"),\n os.path.join(os.path.dirname(__file__), \"crons.yaml\"\
),\n ]\n for c in candidates:\n if c and os.path.exists(c):\n \
\ return os.path.abspath(c)\n raise FileNotFoundError(\"Could not find\
\ crons.yaml. Pass via CRONS_FILE environment variable.\")\n\ndef is_inside_container():\n\
\ return os.path.exists(\"/home/node/.openclaw\") or os.environ.get(\"OPENCLAW_HOME\"\
) is not None\n\ndef run_openclaw_cmd(args):\n if is_inside_container():\n\
\ cmd = [\"openclaw\"] + args\n else:\n namespace = os.environ.get(\"\
NAMESPACE\", \"openclaw\")\n pod = os.environ.get(\"POD_NAME\", \"openclaw-0\"\
)\n container = os.environ.get(\"CONTAINER_NAME\", \"openclaw\")\n \
\ cmd = [\"kubectl\", \"exec\", \"-n\", namespace, pod, \"-c\", container,\
\ \"--\", \"openclaw\"] + args\n \n res = subprocess.run(cmd, capture_output=True,\
\ text=True)\n if res.returncode != 0:\n raise RuntimeError(f\"OpenClaw\
\ CLI command failed ({' '.join(cmd)}):\\n{res.stderr}\\n{res.stdout}\")\n \
\ return res.stdout\n\ndef list_existing_jobs():\n raw = run_openclaw_cmd([\"\
cron\", \"list\", \"--json\"])\n try:\n data = json.loads(raw)\n \
\ return data.get(\"jobs\", [])\n except Exception as e:\n print(f\"\
Warning: could not parse cron list json: {e}\\nRaw output: {raw[:300]}\", file=sys.stderr)\n\
\ return []\n\ndef reconcile_jobs():\n file_path = find_crons_file()\n\
\ print(f\"=== Syncing OpenClaw Crons-as-Code from {file_path} ===\")\n \
\ \n with open(file_path, \"r\", encoding=\"utf-8\") as f:\n spec =\
\ yaml.safe_load(f)\n\n declarations = spec.get(\"jobs\", [])\n existing_jobs\
\ = list_existing_jobs()\n \n print(f\"Found {len(declarations)} declared\
\ jobs in YAML, {len(existing_jobs)} active jobs on gateway.\")\n\n for job\
\ in declarations:\n name = job.get(\"name\")\n decl_key = job.get(\"\
declarationKey\") or name\n desc = job.get(\"description\", \"\")\n \
\ sched = job.get(\"schedule\", {})\n \n # Match existing job\
\ by declarationKey or exact name\n match = next((j for j in existing_jobs\
\ if j.get(\"declarationKey\") == decl_key or j.get(\"name\") == name), None)\n\
\ \n if match:\n existing_id = match.get(\"id\")\n \
\ print(f\"Removing existing job '{name}' (ID {existing_id}) for idempotent\
\ recreation...\")\n try:\n run_openclaw_cmd([\"cron\"\
, \"rm\", existing_id])\n except Exception as e:\n print(f\"\
Warning removing {existing_id}: {e}\")\n\n # Build add command\n \
\ cmd = [\n \"cron\", \"add\",\n \"--name\", name,\n \
\ \"--declaration-key\", decl_key,\n \"--description\", desc,\n\
\ ]\n\n if sched.get(\"kind\") == \"every\":\n cmd.extend([\"\
--every\", sched.get(\"every\", \"15m\")])\n elif sched.get(\"kind\") ==\
\ \"cron\":\n cmd.extend([\"--cron\", sched.get(\"expr\", \"* * * *\
\ *\")])\n if sched.get(\"tz\"):\n cmd.extend([\"--tz\"\
, sched.get(\"tz\")])\n\n if job.get(\"session\"):\n cmd.extend([\"\
--session\", job.get(\"session\")])\n\n if job.get(\"delivery\") == \"\
none\":\n cmd.append(\"--no-deliver\")\n\n if job.get(\"command\"\
):\n cmd.extend([\"--command\", job.get(\"command\")])\n\n if\
\ job.get(\"agent\"):\n cmd.extend([\"--agent\", job.get(\"agent\"\
)])\n\n if job.get(\"model\"):\n cmd.extend([\"--model\", job.get(\"\
model\")])\n\n if job.get(\"channel\"):\n cmd.extend([\"--channel\"\
, job.get(\"channel\")])\n\n if job.get(\"to\"):\n cmd.extend([\"\
--to\", job.get(\"to\")])\n\n if job.get(\"threadId\"):\n cmd.extend([\"\
--thread-id\", str(job.get(\"threadId\"))])\n\n if job.get(\"announce\"\
):\n cmd.append(\"--announce\")\n\n if job.get(\"message\"):\n\
\ cmd.extend([\"--message\", job.get(\"message\")])\n\n print(f\"\
Registering job '{name}'...\")\n run_openclaw_cmd(cmd)\n print(f\"\
Successfully registered '{name}'.\")\n\n print(\"=== OpenClaw Crons-as-Code\
\ reconciliation complete! ===\")\n\nif __name__ == \"__main__\":\n reconcile_jobs()\n"
triage.py: "#!/usr/bin/env python3\n\"\"\"\nopenclaw-triage.py: Rapid zero-dependency\
\ incident triage for OpenClaw multi-agent sessions.\nDirectly inspects ground-truth\
\ SQLite session state and transcript events in < 1 second.\n\"\"\"\nimport argparse\n\
import glob\nimport json\nimport os\nimport sqlite3\nimport sys\nimport time\n\
\nTOPIC_TO_AGENT = {\n \"20\": \"fitness\",\n \"21\": \"koch\",\n \"\
22\": \"hebamme\",\n \"51\": \"travel\",\n \"115\": \"shopping\",\n \"\
116\": \"regensburg\",\n \"117\": \"recherche\",\n \"118\": \"doktor\",\n\
\ \"119\": \"itguy\",\n \"164\": \"grabengasse_media\",\n \"2796\": \"\
mila_tracker\",\n \"2797\": \"nutrition_tracker\",\n}\n\ndef parse_args():\n\
\ parser = argparse.ArgumentParser(description=\"Rapid OpenClaw multi-agent\
\ session triage.\")\n parser.add_argument(\"--topic\", \"-t\", help=\"Telegram\
\ forum topic ID (e.g. 21, 22, 117)\")\n parser.add_argument(\"--agent\", \"\
-a\", help=\"Agent identifier (e.g. koch, hebamme, recherche)\")\n parser.add_argument(\"\
--limit\", \"-n\", type=int, default=10, help=\"Number of recent events to display\
\ (default: 10)\")\n parser.add_argument(\"--db-root\", default=\"/home/node/.openclaw/agents\"\
, help=\"Root path of agent SQLite databases\")\n return parser.parse_args()\n\
\ndef triage_agent_session(agent, topic_id=None, limit=10, db_root=\"/home/node/.openclaw/agents\"\
):\n db_path = os.path.join(db_root, agent, \"agent\", \"openclaw-agent.sqlite\"\
)\n if not os.path.exists(db_path):\n print(f\"[-] Database not found\
\ for agent '{agent}': {db_path}\")\n return False\n\n try:\n \
\ con = sqlite3.connect(db_path, timeout=5.0)\n except Exception as e:\n \
\ print(f\"[-] Failed to connect to SQLite DB for agent '{agent}': {e}\"\
)\n return False\n\n cur = con.cursor()\n # Find matching session\n\
\ if topic_id:\n target_pattern = f\"%topic:{topic_id}%\"\n cur.execute(\n\
\ \"SELECT session_key, current_session_id, status, updated_at, entry_json\
\ FROM session_nodes WHERE session_key LIKE ? AND session_key NOT LIKE '%:heartbeat'\
\ ORDER BY updated_at DESC LIMIT 1\",\n (target_pattern,)\n \
\ )\n else:\n cur.execute(\n \"SELECT session_key, current_session_id,\
\ status, updated_at, entry_json FROM session_nodes WHERE session_key NOT LIKE\
\ '%:heartbeat' ORDER BY updated_at DESC LIMIT 1\"\n )\n\n row = cur.fetchone()\n\
\ if not row:\n print(f\"[-] No session found in {agent} DB (filter:\
\ topic={topic_id})\")\n con.close()\n return False\n\n skey,\
\ sid, status, updated_at_ms, entry_json = row\n now_ms = time.time() * 1000\n\
\ age_min = (now_ms - updated_at_ms) / 60000.0 if updated_at_ms else 0.0\n\n\
\ entry = {}\n if entry_json:\n try:\n entry = json.loads(entry_json)\n\
\ except Exception:\n pass\n\n tokens = entry.get(\"totalTokens\"\
, \"?\")\n model = entry.get(\"model\", \"default\")\n last_run_id = entry.get(\"\
activeWriterRunId\") or entry.get(\"activeRunId\", None)\n\n # Detect anomalies\n\
\ badges = []\n if isinstance(tokens, int):\n if tokens > 60000:\n\
\ badges.append(\"\U0001F6A8 [CRITICAL_CONTEXT_BLOAT: >60k tokens]\"\
)\n elif tokens > 20000:\n badges.append(\"\u26A0\uFE0F [HIGH_CONTEXT_TOKENS:\
\ >20k tokens]\")\n if status == \"running\" and age_min > 5.0:\n badges.append(\"\
\U0001F512 [POSSIBLY_WEDGED_RUNNING: updated >5m ago]\")\n elif status == \"\
processing\":\n badges.append(\"\u26A1 [ACTIVE_PROCESSING]\")\n\n #\
\ Check ingress queue status\n state_db = \"/home/node/.openclaw/state/openclaw.sqlite\"\
\n if topic_id and os.path.exists(state_db):\n try:\n s_con\
\ = sqlite3.connect(state_db, timeout=2.0)\n lane_pat = f\"%topic:{topic_id}%\"\
\n pending_cnt = s_con.execute(\n \"SELECT count(*)\
\ FROM channel_ingress_events WHERE lane_key LIKE ? AND status IN ('pending',\
\ 'claimed')\",\n (lane_pat,)\n ).fetchone()[0]\n \
\ s_con.close()\n if pending_cnt > 0:\n badges.append(f\"\
\U0001F4EC [INGRESS_STALLED: {pending_cnt} message(s) pending]\")\n except\
\ Exception:\n pass\n\n print(\"=\" * 72)\n print(f\"\U0001F3AF\
\ AGENT: {agent.upper():<12} | TOPIC: {topic_id or 'N/A':<6} | STATUS: {status.upper()}\"\
)\n print(f\" SESSION KEY: {skey}\")\n print(f\" SESSION ID: {sid}\"\
)\n print(f\" MODEL: {model}\")\n print(f\" TOKENS: {tokens}\
\ | LAST UPDATED: {age_min:.1f}m ago\")\n if badges:\n print(f\" ANOMALIES:\
\ {' '.join(badges)}\")\n print(\"=\" * 72)\n\n # Query transcript events\n\
\ cur.execute(\n \"SELECT seq, created_at, event_json FROM transcript_events\
\ WHERE session_id = ? ORDER BY seq DESC LIMIT ?\",\n (sid, limit)\n \
\ )\n events = cur.fetchall()\n con.close()\n\n if not events:\n \
\ print(\" (No transcript events found)\")\n return True\n\n events.reverse()\n\
\ print(\"\\n\U0001F4DC RECENT TRANSCRIPT EVENTS (Chronological):\")\n \n\
\ last_role = None\n for seq, ca_ms, ej_str in events:\n try:\n \
\ ev = json.loads(ej_str)\n except Exception:\n print(f\"\
[{seq}] <unparseable event json>\")\n continue\n\n etype = ev.get(\"\
type\")\n if etype != \"message\":\n continue\n\n msg\
\ = ev.get(\"message\", {})\n role = msg.get(\"role\")\n last_role\
\ = role\n content = msg.get(\"content\", [])\n\n if role == \"\
user\":\n txt = content if isinstance(content, str) else json.dumps(content)\n\
\ # Check for audio transcript\n if isinstance(content,\
\ str) and \"[Audio]\" in content:\n print(f\"\\n\U0001F449 [SEQ\
\ {seq}] \U0001F3A4 USER AUDIO PROMPT:\")\n for line in content.splitlines()[:6]:\n\
\ print(f\" {line}\")\n if len(content.splitlines())\
\ > 6:\n print(\" ...\")\n else:\n \
\ txt_preview = txt.replace(\"\\n\", \" \")[:240]\n print(f\"\
\\n\U0001F449 [SEQ {seq}] \U0001F464 USER: {txt_preview}\")\n\n elif role\
\ == \"assistant\":\n print(f\"\\n\U0001F916 [SEQ {seq}] ASSISTANT\
\ TURN:\")\n if isinstance(content, list):\n for item\
\ in content:\n if not isinstance(item, dict):\n \
\ continue\n itype = item.get(\"type\")\n \
\ if itype == \"thinking\":\n th = item.get(\"\
thinking\", \"\").replace(\"\\n\", \" \")\n print(f\" \
\ \U0001F9E0 THINKING: {th[:280]}...\")\n elif itype == \"\
toolCall\":\n tname = item.get(\"name\", \"unknown\")\n\
\ targs = json.dumps(item.get(\"arguments\", {}))\n \
\ print(f\" \u2699\uFE0F TOOL CALL: {tname}({targs[:160]})\"\
)\n elif itype == \"text\":\n tx = item.get(\"\
text\", \"\")\n lines = tx.splitlines()\n \
\ prev = lines[0][:150] if lines else \"\"\n \
\ print(f\" \U0001F4AC TEXT OUTPUT: {prev}...\")\n elif isinstance(content,\
\ str):\n print(f\" \U0001F4AC TEXT OUTPUT: {content[:200]}...\"\
)\n\n elif role == \"toolResult\":\n is_err = ev.get(\"isError\"\
, False)\n tname = ev.get(\"details\", {}).get(\"tool\") or msg.get(\"\
toolName\", \"tool\")\n res_txt = json.dumps(content) if not isinstance(content,\
\ str) else content\n if is_err or \"error\" in res_txt.lower() or\
\ \"must have required property\" in res_txt:\n print(f\" \u274C\
\ [SEQ {seq}] TOOL ERROR ({tname}): {res_txt[:260]}\")\n else:\n \
\ print(f\" \u2705 [SEQ {seq}] TOOL RESULT ({tname}): {res_txt[:180]}\"\
)\n\n if last_role == \"user\":\n print(\"\\n\u26A0\uFE0F WARNING: The\
\ last event in the transcript is an UNANSWERED USER MESSAGE!\")\n print(\"\
\ The agent turn either crashed, timed out, or was killed before producing a\
\ response.\")\n\n print(\"\\n\" + \"=\" * 72)\n return True\n\ndef main():\n\
\ args = parse_args()\n if not args.topic and not args.agent:\n print(\"\
Usage: openclaw-triage.py (--topic <ID> | --agent <NAME>) [--limit <N>]\")\n \
\ sys.exit(1)\n\n agent = args.agent\n topic = args.topic\n\n if\
\ topic and not agent:\n agent = TOPIC_TO_AGENT.get(str(topic))\n \
\ if not agent:\n # Search all DBs\n for a, t in TOPIC_TO_AGENT.items():\n\
\ if t == topic:\n agent = a\n \
\ break\n\n if not agent:\n # Scan all DBs for the topic\n \
\ for db in sorted(glob.glob(os.path.join(args.db_root, \"*/agent/openclaw-agent.sqlite\"\
))):\n cand_agent = db.split(os.sep)[-3]\n try:\n \
\ con = sqlite3.connect(db, timeout=2.0)\n cur = con.cursor()\n\
\ cur.execute(\n \"SELECT session_key FROM session_nodes\
\ WHERE session_key LIKE ? LIMIT 1\",\n (f\"%topic:{topic}%\"\
,)\n )\n if cur.fetchone():\n \
\ agent = cand_agent\n con.close()\n break\n\
\ con.close()\n except Exception:\n pass\n\
\n if not agent:\n print(f\"[-] Could not map topic '{topic}' to any\
\ known OpenClaw agent.\")\n sys.exit(1)\n\n success = triage_agent_session(agent,\
\ topic_id=topic, limit=args.limit, db_root=args.db_root)\n sys.exit(0 if success\
\ else 1)\n\nif __name__ == \"__main__\":\n main()\n"
crons.yaml: "# Declarative Crons-as-Code for OpenClaw (Haumdaucher)\n# Reconciled\
\ idempotently via k8s/openclaw/scripts/sync_crons.py\n\nversion: 1\njobs:\n \
\ # 1. Maintenance & State Truncation\n - name: \"tracking-session-sliding-window\"\
\n declarationKey: \"tracking-session-sliding-window\"\n description: \"\
Lightweight sliding-window truncation (max 8 events) for mila_tracker and nutrition_tracker\"\
\n schedule:\n kind: \"every\"\n every: \"15m\"\n session: \"\
isolated\"\n delivery: \"none\"\n command: \"python3 /home/node/.openclaw/scripts/compact_tracking_sessions.py\"\
\n\n - name: \"nightly-session-compaction\"\n declarationKey: \"nightly-session-compaction\"\
\n description: \"Nightly multi-agent session compaction and SQLite WAL checkpointing\"\
\n schedule:\n kind: \"cron\"\n expr: \"45 4 * * *\"\n tz: \"\
Europe/Berlin\"\n session: \"isolated\"\n delivery: \"none\"\n command:\
\ \"python3 /home/node/.openclaw/scripts/compact_sessions.py\"\n\n # 2. Family\
\ Dashboards & Web Analytics\n - name: \"dashboards-periodic-publish\"\n declarationKey:\
\ \"dashboards-periodic-publish\"\n description: \"Periodically regenerates\
\ and uploads Mila & Moritz family HTML dashboards to S3\"\n schedule:\n \
\ kind: \"every\"\n every: \"10m\"\n session: \"isolated\"\n delivery:\
\ \"none\"\n command: \"sh -lc 'export PATH=/home/node/.openclaw/bin:$PATH;\
\ cd /home/node/mop-skills/mop-skills/skills/datastore && uv run scripts/update_website.py\
\ --data-dir /home/node/.openclaw/wiki/main/data && uv run scripts/update_moritz_website.py\
\ --data-dir /home/node/.openclaw/wiki/main/data'\"\n\n # 3. Nightly Garmin Connect\
\ Mirror\n - name: \"garmin-nightly-mirror\"\n declarationKey: \"garmin-nightly-mirror\"\
\n description: \"N\xE4chtlicher deterministischer Spiegel der Garmin Connect\
\ Daten (Aktivit\xE4ten, Daily Summary, Schlaf) in Git JSONL\"\n schedule:\n\
\ kind: \"cron\"\n expr: \"30 3 * * *\"\n tz: \"Europe/Berlin\"\
\n agent: \"fitness\"\n model: \"openrouter/deepseek/deepseek-v4.1-flash\"\
\n session: \"isolated\"\n channel: \"telegram\"\n to: \"-1004326921510\"\
\n threadId: \"20\"\n announce: true\n message: |\n F\xFChre den\
\ n\xE4chtlichen Garmin Connect Sync durch.\n Vorgehen:\n 1. F\xFChre\
\ den Garmin-Sync-Befehl aus:\n uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/garmin_sync.py\
\ --days 2 --data-dir /home/node/.openclaw/wiki/main/data\n 2. Aktualisiere\
\ die gerenderten Markdown-Ansichten:\n PYTHONPATH=/home/node/mop-skills/mop-skills/skills/datastore/scripts\
\ uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/render_views.py\
\ --data-dir /home/node/.openclaw/wiki/main/data --wiki-dir /home/node/.openclaw/wiki/main\n\
\ 3. Aktualisiere das pers\xF6nliche Health & Nutrition Dashboard (moritz.html):\n\
\ uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/update_moritz_website.py\
\ --data-dir /home/node/.openclaw/wiki/main/data\n 4. Git Commit & Push im\
\ Knowledge Wiki:\n cd /home/node/.openclaw/wiki/main && git add data/garmin/\
\ entities/nutrition-log/ public/ && git diff --staged --quiet || (git commit\
\ -m \"chore(garmin): automated nightly mirror [skip ci]\" && git push origin\
\ main)\n 5. Finale Antwort:\n - Bei Erfolg: Gib exakt 'NO_REPLY'\
\ aus (intentional silence, kein Telegram-Spam in der Nacht).\n - Nur\
\ bei Authentifizierungsfehlern: Informiere kurz via Telegram (Thema #Sport),\
\ dass ein Garmin-Re-Auth n\xF6tig ist, oder starte den Re-Auth-Flow.\n\n # 4.\
\ Weekly Autonomous System & Error Audit\n - name: \"itguy-weekly-health-audit\"\
\n declarationKey: \"itguy-weekly-health-audit\"\n description: \"W\xF6\
chentlicher Self-Audit der OpenClaw-Logs, Tool-Failures und Session-Transkripte\
\ mit Git-Commit-Korrelation\"\n schedule:\n kind: \"cron\"\n expr:\
\ \"0 3 * * 0\"\n tz: \"Europe/Berlin\"\n agent: \"itguy\"\n model:\
\ \"openrouter/deepseek/deepseek-v4.1-flash\"\n session: \"isolated\"\n \
\ channel: \"telegram\"\n to: \"-1004326921510\"\n threadId: \"119\"\n \
\ announce: true\n message: |\n F\xFChre den w\xF6chentlichen OpenClaw\
\ System- und Log-Audit durch.\n Vorgehen:\n 1. F\xFChre den Audit-Befehl\
\ aus:\n uv run /home/node/mop-skills/mop-skills/skills/openclaw-audit/scripts/audit_openclaw.py\
\ --since 7d --write-report --add-task --git-push\n 2. Analysiere die Ausgabe\
\ und korreliere eventuelle Fehler-Cluster mit den j\xFCngsten Commits in infrapuzzle,\
\ mop-skills und knowledge-wiki.\n 3. Finale Antwort:\n - Wenn NEUE\
\ Probleme gefunden oder Tasks in tasks/antigravity.md eingetragen wurden: Fasse\
\ die Ergebnisse kurz, pr\xE4zise und l\xF6sungsorientiert zusammen (betroffene\
\ Tools, vermutete Ursache, Link zur Task-Queue) und gib diese Zusammenfassung\
\ als finale Antwort aus.\n - Wenn das System gesund ist und KEINE neuen\
\ Tasks erzeugt wurden: Gib als finale Antwort exakt 'NO_REPLY' aus (intentional\
\ silence, kein Telegram-Spam).\n\n # 5. Weekly AI Roundup (Recherche)\n - name:\
\ \"weekly-ai-roundup\"\n declarationKey: \"weekly-ai-roundup:recherche\"\n\
\ description: \"W\xF6chentlicher AI-Roundup und Tech-Scanning f\xFCr Topic\
\ #Research\"\n schedule:\n kind: \"cron\"\n expr: \"30 6 * * 5\"\
\n tz: \"Europe/Berlin\"\n agent: \"recherche\"\n model: \"openrouter/deepseek/deepseek-v4.1-flash\"\
\n session: \"isolated\"\n channel: \"telegram\"\n to: \"-1004326921510\"\
\n threadId: \"117\"\n announce: true\n message: |\n \U0001F5D3\uFE0F\
\ W\xD6CHENTLICHER AI-ROUNDUP (Freitag). F\xFChre den in sources.md im Workspace\
\ definierten Ablauf aus und poste das Ergebnis als EINE kompakte Telegram-Nachricht\
\ in diesen Topic (#Research).\n\n 1) Hacker News: Algolia API \u2013 https://hn.algolia.com/api/v1/search_by_date?tags=story&numericFilters=points>150,created_at_i>{Unix-Timestamp\
\ von vor 7 Tagen}&hitsPerPage=100 \u2192 daraus die KI-relevanten Top-Stories\
\ nach Punkten ziehen.\n 2) Frontier-Labs pr\xFCfen: OpenAI, Anthropic, Google\
\ DeepMind, Meta AI, Mistral, xAI.\n 3) China-Labs pr\xFCfen: GLM/Z.ai, Qwen,\
\ DeepSeek, Moonshot/Kimi, MiniMax.\n 4) Tracker: Artificial Analysis Releases,\
\ Hugging Face Trending, OpenRouter Rankings, LMArena.\n 5) Newsletter/Blogs:\
\ The Batch, Import AI, Interconnects, Ahead of AI, Simon Willison, TLDR AI.\n\
\ 6) GitHub: Trending (weekly) + neue Releases der Kern-Orgs (openai, anthropics,\
\ google-deepmind, QwenLM, deepseek-ai, zai-org, meta-llama, mistralai).\n \
\ 7) YouTube nur als Hype-Indikator (Titel/Snippets), KEINE Transkripte.\n\n\
\ Ausgabe (Deutsch, Markdown, KEINE Tabellen):\n - Top-3-Highlights\
\ der Woche (je 1 Satz + Link)\n - Danach Bullets je Kategorie: \u2460 Frontier-Modelle\
\ \xB7 \u2461 China-Modelle \xB7 \u2462 Tools/Infra/Procedures \xB7 \u2463 Trends\
\ (HN/GitHub/YouTube), je Eintrag genau 1 Quell-Link\n - Scope 60/40 (Modelle/Tools).\
\ Wenn eine Kategorie nichts Neues hat: kurz 'nichts Neues'.\n - Am Ende\
\ NUR falls n\xF6tig eine Zeile: welche Quelle in sources.md erg\xE4nzt/entfernt\
\ werden sollte.\n\n Nutze web_search/web_fetch. Effort: medium.\n\n # 6.\
\ Weekly Bridge Tracker (Holzgartensteg)\n - name: \"Recherche: Holzgartensteg\
\ (w\xF6chentlich)\"\n declarationKey: \"recherche:holzgartensteg\"\n description:\
\ \"W\xF6chentlicher Status-Check zum Holzgartensteg Regensburg via ALLRIS & Web\"\
\n schedule:\n kind: \"cron\"\n expr: \"0 16 * * 5\"\n tz: \"\
Europe/Berlin\"\n agent: \"main\"\n session: \"isolated\"\n channel:\
\ \"telegram\"\n to: \"306373425\"\n announce: true\n message: |\n \
\ W\xF6chentlicher Recherche-Task (Holzgartensteg, Stadt Regensburg).\n\n \
\ AUFGABE: Pr\xFCfe, ob es NEUIGKEITEN zum geplanten Holzgartensteg (geplante\
\ Fu\xDF-/Radbr\xFCcke zwischen Grieser Spitz und Maria-Beer-Platz / Weichs) gibt\
\ \u2014 seit dem letzten Check bzw. den letzten ~7 Tagen.\n\n QUELLEN:\n\
\ 1) ALLRIS via Skill 'regensburg-stadtrat': Script /home/node/mop-skills/mop-skills/skills/regensburg-stadtrat/scripts/regensburg_ris.py\
\ \u2014 Befehle: search \"Holzgartensteg\" --limit 30 ; calendar --days 30 ;\
\ vorlage <ID> ; session <ID>. Achte auf NEUE Vorlagen, Beschl\xFCsse, Anfragen\
\ und Sitzungstermine (Ausschuss f\xFCr Stadtplanung, Verkehr und Wohnungsfragen\
\ / Stadtrat).\n 2) Lokale Medien via web_search: regensburg-digital.de,\
\ idowa.de, mittelbayerische.de, tvaktuell.com, regensburger-nachrichten.de.\n\
\n FOKUS: neue Vorlagen/Beschl\xFCsse, Ausschuss-/Stadtrattermine, Mittel/Budget,\
\ Planungsstand (Variante, Kosten, Termin), politische Positionen.\n\n BEKANNTER\
\ STAND (nicht wiederholen, nur Abweichungen melden): 2020 aus Investitionsprogramm\
\ gestrichen ('beerdigt'); Comeback ab 2025 (Gr\xFCne/Verb\xE4nde); Kommunalwahl\
\ 2026 mehrere Parteien daf\xFCr; Haushalt 2026: ~100.000 EUR Planungsmittel im\
\ Investitionsprogramm, Stadtrat-Beschluss 26.02.2026, Genehmigung Regierung der\
\ Oberpfalz 06.03.2026; Koalitionsvereinbarung 2026 mit Wiederaufnahme der Planungen.\n\
\n AUSGABE: Kurz auf Deutsch an Moritz. Falls echte Neuigkeiten: 3-6 Bullets\
\ mit Quelle+Link. Falls NICHTS Neues: eine kurze Zeile, z.B. 'Holzgartensteg:\
\ keine neuen Entwicklungen diese Woche.' Kein Hintergrund-Recycling.\n"
Binary file not shown.
+154
View File
@@ -0,0 +1,154 @@
# Declarative Crons-as-Code for OpenClaw (Haumdaucher)
# Reconciled idempotently via k8s/openclaw/scripts/sync_crons.py
version: 1
jobs:
# 1. Maintenance & State Truncation
- name: "tracking-session-sliding-window"
declarationKey: "tracking-session-sliding-window"
description: "Lightweight sliding-window truncation (max 8 events) for mila_tracker and nutrition_tracker"
schedule:
kind: "every"
every: "15m"
session: "isolated"
delivery: "none"
command: "python3 /home/node/.openclaw/scripts/compact_tracking_sessions.py"
- name: "nightly-session-compaction"
declarationKey: "nightly-session-compaction"
description: "Nightly multi-agent session compaction and SQLite WAL checkpointing"
schedule:
kind: "cron"
expr: "45 4 * * *"
tz: "Europe/Berlin"
session: "isolated"
delivery: "none"
command: "python3 /home/node/.openclaw/scripts/compact_sessions.py"
# 2. Family Dashboards & Web Analytics
- name: "dashboards-periodic-publish"
declarationKey: "dashboards-periodic-publish"
description: "Periodically regenerates and uploads Mila & Moritz family HTML dashboards to S3"
schedule:
kind: "every"
every: "10m"
session: "isolated"
delivery: "none"
command: "sh -lc 'export PATH=/home/node/.openclaw/bin:$PATH; cd /home/node/mop-skills/mop-skills/skills/datastore && uv run scripts/update_website.py --data-dir /home/node/.openclaw/wiki/main/data && uv run scripts/update_moritz_website.py --data-dir /home/node/.openclaw/wiki/main/data'"
# 3. Nightly Garmin Connect Mirror
- name: "garmin-nightly-mirror"
declarationKey: "garmin-nightly-mirror"
description: "Nächtlicher deterministischer Spiegel der Garmin Connect Daten (Aktivitäten, Daily Summary, Schlaf) in Git JSONL"
schedule:
kind: "cron"
expr: "30 3 * * *"
tz: "Europe/Berlin"
agent: "fitness"
model: "openrouter/deepseek/deepseek-v4.1-flash"
session: "isolated"
channel: "telegram"
to: "-1004326921510"
threadId: "20"
announce: true
message: |
Führe den nächtlichen Garmin Connect Sync durch.
Vorgehen:
1. Führe den Garmin-Sync-Befehl aus:
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/garmin_sync.py --days 2 --data-dir /home/node/.openclaw/wiki/main/data
2. Aktualisiere die gerenderten Markdown-Ansichten:
PYTHONPATH=/home/node/mop-skills/mop-skills/skills/datastore/scripts uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/render_views.py --data-dir /home/node/.openclaw/wiki/main/data --wiki-dir /home/node/.openclaw/wiki/main
3. Aktualisiere das persönliche Health & Nutrition Dashboard (moritz.html):
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/update_moritz_website.py --data-dir /home/node/.openclaw/wiki/main/data
4. Git Commit & Push im Knowledge Wiki:
cd /home/node/.openclaw/wiki/main && git add data/garmin/ entities/nutrition-log/ public/ && git diff --staged --quiet || (git commit -m "chore(garmin): automated nightly mirror [skip ci]" && git push origin main)
5. Finale Antwort:
- Bei Erfolg: Gib exakt 'NO_REPLY' aus (intentional silence, kein Telegram-Spam in der Nacht).
- Nur bei Authentifizierungsfehlern: Informiere kurz via Telegram (Thema #Sport), dass ein Garmin-Re-Auth nötig ist, oder starte den Re-Auth-Flow.
# 4. Weekly Autonomous System & Error Audit
- name: "itguy-weekly-health-audit"
declarationKey: "itguy-weekly-health-audit"
description: "Wöchentlicher Self-Audit der OpenClaw-Logs, Tool-Failures und Session-Transkripte mit Git-Commit-Korrelation"
schedule:
kind: "cron"
expr: "0 3 * * 0"
tz: "Europe/Berlin"
agent: "itguy"
model: "openrouter/deepseek/deepseek-v4.1-flash"
session: "isolated"
channel: "telegram"
to: "-1004326921510"
threadId: "119"
announce: true
message: |
Führe den wöchentlichen OpenClaw System- und Log-Audit durch.
Vorgehen:
1. Führe den Audit-Befehl aus:
uv run /home/node/mop-skills/mop-skills/skills/openclaw-audit/scripts/audit_openclaw.py --since 7d --write-report --add-task --git-push
2. Analysiere die Ausgabe und korreliere eventuelle Fehler-Cluster mit den jüngsten Commits in infrapuzzle, mop-skills und knowledge-wiki.
3. Finale Antwort:
- Wenn NEUE Probleme gefunden oder Tasks in tasks/antigravity.md eingetragen wurden: Fasse die Ergebnisse kurz, präzise und lösungsorientiert zusammen (betroffene Tools, vermutete Ursache, Link zur Task-Queue) und gib diese Zusammenfassung als finale Antwort aus.
- Wenn das System gesund ist und KEINE neuen Tasks erzeugt wurden: Gib als finale Antwort exakt 'NO_REPLY' aus (intentional silence, kein Telegram-Spam).
# 5. Weekly AI Roundup (Recherche)
- name: "weekly-ai-roundup"
declarationKey: "weekly-ai-roundup:recherche"
description: "Wöchentlicher AI-Roundup und Tech-Scanning für Topic #Research"
schedule:
kind: "cron"
expr: "30 6 * * 5"
tz: "Europe/Berlin"
agent: "recherche"
model: "openrouter/deepseek/deepseek-v4.1-flash"
session: "isolated"
channel: "telegram"
to: "-1004326921510"
threadId: "117"
announce: true
message: |
🗓️ WÖCHENTLICHER AI-ROUNDUP (Freitag). Führe den in sources.md im Workspace definierten Ablauf aus und poste das Ergebnis als EINE kompakte Telegram-Nachricht in diesen Topic (#Research).
1) Hacker News: Algolia API – https://hn.algolia.com/api/v1/search_by_date?tags=story&numericFilters=points>150,created_at_i>{Unix-Timestamp von vor 7 Tagen}&hitsPerPage=100 → daraus die KI-relevanten Top-Stories nach Punkten ziehen.
2) Frontier-Labs prüfen: OpenAI, Anthropic, Google DeepMind, Meta AI, Mistral, xAI.
3) China-Labs prüfen: GLM/Z.ai, Qwen, DeepSeek, Moonshot/Kimi, MiniMax.
4) Tracker: Artificial Analysis Releases, Hugging Face Trending, OpenRouter Rankings, LMArena.
5) Newsletter/Blogs: The Batch, Import AI, Interconnects, Ahead of AI, Simon Willison, TLDR AI.
6) GitHub: Trending (weekly) + neue Releases der Kern-Orgs (openai, anthropics, google-deepmind, QwenLM, deepseek-ai, zai-org, meta-llama, mistralai).
7) YouTube nur als Hype-Indikator (Titel/Snippets), KEINE Transkripte.
Ausgabe (Deutsch, Markdown, KEINE Tabellen):
- Top-3-Highlights der Woche (je 1 Satz + Link)
- Danach Bullets je Kategorie: ① Frontier-Modelle · ② China-Modelle · ③ Tools/Infra/Procedures · ④ Trends (HN/GitHub/YouTube), je Eintrag genau 1 Quell-Link
- Scope 60/40 (Modelle/Tools). Wenn eine Kategorie nichts Neues hat: kurz 'nichts Neues'.
- Am Ende NUR falls nötig eine Zeile: welche Quelle in sources.md ergänzt/entfernt werden sollte.
Nutze web_search/web_fetch. Effort: medium.
# 6. Weekly Bridge Tracker (Holzgartensteg)
- name: "Recherche: Holzgartensteg (wöchentlich)"
declarationKey: "recherche:holzgartensteg"
description: "Wöchentlicher Status-Check zum Holzgartensteg Regensburg via ALLRIS & Web"
schedule:
kind: "cron"
expr: "0 16 * * 5"
tz: "Europe/Berlin"
agent: "main"
session: "isolated"
channel: "telegram"
to: "306373425"
announce: true
message: |
Wöchentlicher Recherche-Task (Holzgartensteg, Stadt Regensburg).
AUFGABE: Prüfe, ob es NEUIGKEITEN zum geplanten Holzgartensteg (geplante Fuß-/Radbrücke zwischen Grieser Spitz und Maria-Beer-Platz / Weichs) gibt — seit dem letzten Check bzw. den letzten ~7 Tagen.
QUELLEN:
1) ALLRIS via Skill 'regensburg-stadtrat': Script /home/node/mop-skills/mop-skills/skills/regensburg-stadtrat/scripts/regensburg_ris.py — Befehle: search "Holzgartensteg" --limit 30 ; calendar --days 30 ; vorlage <ID> ; session <ID>. Achte auf NEUE Vorlagen, Beschlüsse, Anfragen und Sitzungstermine (Ausschuss für Stadtplanung, Verkehr und Wohnungsfragen / Stadtrat).
2) Lokale Medien via web_search: regensburg-digital.de, idowa.de, mittelbayerische.de, tvaktuell.com, regensburger-nachrichten.de.
FOKUS: neue Vorlagen/Beschlüsse, Ausschuss-/Stadtrattermine, Mittel/Budget, Planungsstand (Variante, Kosten, Termin), politische Positionen.
BEKANNTER STAND (nicht wiederholen, nur Abweichungen melden): 2020 aus Investitionsprogramm gestrichen ('beerdigt'); Comeback ab 2025 (Grüne/Verbände); Kommunalwahl 2026 mehrere Parteien dafür; Haushalt 2026: ~100.000 EUR Planungsmittel im Investitionsprogramm, Stadtrat-Beschluss 26.02.2026, Genehmigung Regierung der Oberpfalz 06.03.2026; Koalitionsvereinbarung 2026 mit Wiederaufnahme der Planungen.
AUSGABE: Kurz auf Deutsch an Moritz. Falls echte Neuigkeiten: 3-6 Bullets mit Quelle+Link. Falls NICHTS Neues: eine kurze Zeile, z.B. 'Holzgartensteg: keine neuen Entwicklungen diese Woche.' Kein Hintergrund-Recycling.
Binary file not shown.
+205
View File
@@ -0,0 +1,205 @@
#!/bin/sh
set -e
echo "=== OpenClaw Dependency & Runtime Bootstrap ==="
export PATH="/home/node/.openclaw/bin:/home/node/.openclaw/.local/bin:$PATH"
SCRIPTS_DIR="/home/node/.openclaw/scripts"
mkdir -p /home/node/.openclaw/bin "$SCRIPTS_DIR" /home/node/.openclaw/.config
touch /home/node/.openclaw/.config/keep
# 1. Sanitize legacy config keys in openclaw.json
node -e '
const fs = require("fs");
for (const file of ["/home/node/.openclaw/openclaw.json", "/home/openclaw/.openclaw/openclaw.json"]) {
if (fs.existsSync(file)) {
try {
const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
if (cfg.browser) {
delete cfg.browser.remoteCdpTimeoutMs;
if (cfg.browser.profiles) {
if (cfg.browser.profiles.default) delete cfg.browser.profiles.default.color;
if (cfg.browser.profiles.chrome) delete cfg.browser.profiles.chrome;
}
}
if (cfg.gateway && cfg.gateway.controlUi) {
delete cfg.gateway.controlUi.dangerouslyDisableDeviceAuth;
}
if (!cfg.agents) cfg.agents = {};
cfg.agents.ownership = "explicit";
if (cfg.agents.entries && cfg.agents.entries.main) {
delete cfg.agents.entries.main.default;
}
fs.writeFileSync(file, JSON.stringify(cfg, null, 2));
console.log("Sanitized config file:", file);
} catch (e) {
console.error("Failed to sanitize", file, e.message);
}
}
}
' || true
# 2. Run pre-flight session recovery
echo "Running pre-flight self-healing..."
python3 "$SCRIPTS_DIR/recover_sessions.py" || true
# 3. Doctor fix
echo "Running openclaw doctor --fix to migrate runtime config on PVC..."
openclaw doctor --fix || true
# 4. Garmin OAuth tokens bootstrap / preserve
if [ ! -s /home/node/.openclaw/garmin_tokens/garmin_tokens.json ] && [ -n "$GARMIN_TOKENS_BASE64" ]; then
echo "Bootstrapping initial Garmin OAuth tokens from secret..."
mkdir -p /home/node/.openclaw/garmin_tokens
echo "$GARMIN_TOKENS_BASE64" | base64 -d > /home/node/.openclaw/garmin_tokens/garmin_tokens.json
chmod 700 /home/node/.openclaw/garmin_tokens
chmod 600 /home/node/.openclaw/garmin_tokens/garmin_tokens.json
elif [ -s /home/node/.openclaw/garmin_tokens/garmin_tokens.json ]; then
echo "Preserving live Garmin OAuth tokens on PVC..."
chmod 700 /home/node/.openclaw/garmin_tokens
chmod 600 /home/node/.openclaw/garmin_tokens/garmin_tokens.json
fi
# 5. Cleanup legacy binaries and superseded local skills
rm -rf /home/node/.openclaw/bin/whisper* /home/node/.openclaw/bin/.whisper* /home/node/.openclaw/scripts/whisper* /home/node/.openclaw/models/whisper || true
rm -rf /home/node/.openclaw/skills/memobird /home/node/.openclaw/skills/nerd-report /home/node/.openclaw/skills/rvv-regensburg /home/node/.openclaw/skills/skill-creator || true
rm -rf /home/node/.openclaw/workspace/skills /home/node/.openclaw/workspace-koch/skills /home/node/.openclaw/workspace-recherche/skills || true
# 6. Ensure tools.allow and models.providers.openrouter.apiKey unset
openclaw config unset tools.allow || true
openclaw config unset models.providers.openrouter.apiKey || true
openclaw config unset agents.entries.mila_tracker.tools.profile || true
openclaw config unset agents.entries.nutrition_tracker.tools.profile || true
# 7. Decouple hardlinked bootstrap files
for dir in /home/node/.openclaw/workspace-*; do
if [ -d "$dir" ]; then
for file in USER.md MEMORY.md; do
if [ -f "/home/node/.openclaw/workspace/$file" ]; then
cp --remove-destination "/home/node/.openclaw/workspace/$file" "$dir/$file"
chown node:node "$dir/$file" 2>/dev/null || true
chmod 664 "$dir/$file" 2>/dev/null || true
fi
done
fi
done
# 8. Install CLI binaries
if [ ! -f "/home/node/.openclaw/bin/openhue.real" ]; then
echo "Installing openhue CLI binary..."
curl -sL https://github.com/openhue/openhue-cli/releases/download/0.24/openhue_Linux_x86_64.tar.gz | tar -xz -C /home/node/.openclaw/bin openhue
mv /home/node/.openclaw/bin/openhue /home/node/.openclaw/bin/openhue.real
printf '#!/bin/sh\nexport HOME=/home/node/.openclaw\nexec /home/node/.openclaw/bin/openhue.real "$@"\n' > /home/node/.openclaw/bin/openhue
chmod +x /home/node/.openclaw/bin/openhue /home/node/.openclaw/bin/openhue.real
fi
if [ ! -f "/home/node/.openclaw/bin/summarize" ]; then
echo "Installing summarize CLI via npm..."
npm install -g --prefix /home/node/.openclaw @steipete/summarize || true
fi
if [ ! -f "/home/node/.openclaw/bin/mcp-server-github" ]; then
echo "Installing @modelcontextprotocol/server-github via npm..."
npm install -g --prefix /home/node/.openclaw @modelcontextprotocol/server-github || true
fi
if [ ! -f "/home/node/.openclaw/bin/kubectl" ]; then
echo "Installing kubectl CLI binary..."
curl -fsSL https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl -o /home/node/.openclaw/bin/kubectl
chmod +x /home/node/.openclaw/bin/kubectl
fi
if [ ! -f "/home/node/.openclaw/bin/jq" ]; then
echo "Installing jq CLI binary..."
curl -fsSL https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64 -o /home/node/.openclaw/bin/jq
chmod +x /home/node/.openclaw/bin/jq
fi
if [ ! -f "/home/node/.openclaw/bin/file" ]; then
echo "Setting up file CLI helper..."
cp "$SCRIPTS_DIR/file_cli.py" /home/node/.openclaw/bin/file
chmod +x /home/node/.openclaw/bin/file
fi
if [ -x "/home/node/.openclaw/.local/bin/python3" ]; then
/home/node/.openclaw/bin/uv pip install --break-system-packages --python /home/node/.openclaw/.local/bin/python3 pillow google-api-python-client pypdf || true
fi
# 9. Clone or pull infrapuzzle repository to PVC
mkdir -p /home/node/.openclaw/repos
if [ ! -d "/home/node/.openclaw/repos/infrapuzzle/.git" ]; then
echo "Cloning infrapuzzle repository to PVC..."
git clone --depth 1 "https://${GIT_SYNC_USERNAME}:${GIT_SYNC_PASSWORD}@git.moritzgraf.de/moritz/infrapuzzle.git" /home/node/.openclaw/repos/infrapuzzle || true
else
echo "Pulling latest infrapuzzle commits on PVC..."
git -C /home/node/.openclaw/repos/infrapuzzle pull origin master || git -C /home/node/.openclaw/repos/infrapuzzle pull origin main || true
fi
# 10. Seed and maintain persistent mcp-venv
if [ ! -d "/home/node/.openclaw/mcp-venv" ]; then
echo "Seeding persistent mcp-venv..."
uv venv --python 3.12 /home/node/.openclaw/mcp-venv
uv pip install --python /home/node/.openclaw/mcp-venv \
prometheus-mcp-server \
ha-mcp \
chuk-mcp-open-meteo \
gitea-mcp \
git+https://github.com/Taxuspt/garmin_mcp \
"mcp>=2.0" \
filetype \
google-api-python-client \
google-auth \
bring-api \
requests \
beautifulsoup4 \
youtube-transcript-api
fi
if [ -d "/home/node/.openclaw/mcp-venv" ]; then
python3 -c 'import os; p="/home/node/.openclaw/mcp-venv/lib/python3.12/site-packages/mcp/server/fastmcp.py"; open(p, "w").write("import os\nfrom fastmcp import FastMCP as _FastMCP, Context\n\nclass FastMCP(_FastMCP):\n def __init__(self, name=\"MCP Server\", host=None, port=None, **kwargs):\n if host is not None: os.environ[\"FASTMCP_HOST\"] = str(host)\n if port is not None: os.environ[\"FASTMCP_PORT\"] = str(port)\n super().__init__(name=name, **kwargs)\n")' || true
python3 "$SCRIPTS_DIR/patch_garmin_mcp.py" || true
fi
# 11. Extensions & Plugins
if [ ! -d "/home/node/.openclaw/extensions/perplexity" ]; then
echo "Installing perplexity plugin..."
openclaw plugins install clawhub:@openclaw/perplexity-plugin --accept-capabilities || true
fi
echo "Enabling memory-wiki plugin..."
openclaw plugins enable memory-wiki || true
echo "Patching perplexity web search runtime for chat completions compatibility..."
node "$SCRIPTS_DIR/patch_perplexity.mjs" || true
# 12. Ensure OpenRouter auth profile in state DB
echo "Ensuring OpenRouter auth profile in state DB..."
node --input-type=module -e '
import { DatabaseSync } from "node:sqlite";
const key = process.env.OPENROUTER_API_KEY;
if (key) {
try {
const dbPath = "/home/node/.openclaw/state/openclaw.sqlite";
const db = new DatabaseSync(dbPath);
db.prepare("CREATE TABLE IF NOT EXISTS config_machine_state (state_key TEXT PRIMARY KEY, value_json TEXT NOT NULL, updated_at_ms INTEGER NOT NULL)").run();
const row = db.prepare("SELECT value_json FROM config_machine_state WHERE state_key = \x27authProfiles.store\x27").get();
let store = row?.value_json ? JSON.parse(row.value_json) : { version: 1, profiles: {} };
if (!store.profiles) store.profiles = {};
store.profiles["openrouter:default"] = {
type: "api_key",
provider: "openrouter",
key: key
};
db.prepare("INSERT INTO config_machine_state (state_key, value_json, updated_at_ms) VALUES (\x27authProfiles.store\x27, ?, ?) ON CONFLICT(state_key) DO UPDATE SET value_json = excluded.value_json, updated_at_ms = excluded.updated_at_ms")
.run(JSON.stringify(store), Date.now());
console.log("Synchronized openrouter auth profile in state DB.");
} catch (e) {
console.error("Could not sync auth profile:", e.message);
}
}
' || true
# 13. Pre-warm SQLite page cache
echo "Pre-warming SQLite database files into OS page cache for instant cold start..."
cat /home/node/.openclaw/state/openclaw.sqlite /home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite > /dev/null 2>&1 || true
echo "=== Bootstrap Complete ==="
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
import glob, os, sqlite3, sys
vec_path = "/app/node_modules/sqlite-vec-linux-x64/vec0.so"
for db in sorted(glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")):
agent = os.path.basename(os.path.dirname(os.path.dirname(db)))
if agent == "main":
continue
try:
conn = sqlite3.connect(db, timeout=10.0)
if os.path.exists(vec_path):
conn.enable_load_extension(True)
conn.load_extension(vec_path)
cur = conn.cursor()
chk = cur.execute("SELECT count(*) FROM memory_index_chunks").fetchone()[0]
if chk > 0:
print(f"[dedup_subagents] Purging {chk} duplicated vector chunks from agent {agent}...")
for t in [
"memory_index_sources",
"memory_index_chunks",
"memory_index_chunk_recall_metadata",
"memory_index_chunk_provenance",
"memory_entry_origins",
"memory_embedding_cache",
"memory_index_chunks_vec"
]:
try: cur.execute(f"DELETE FROM {t}")
except Exception: pass
conn.commit()
cur.execute("VACUUM")
print(f"[dedup_subagents] Reclaimed disk space for agent {agent}")
conn.close()
except Exception as e:
print(f"[dedup_subagents] Notice for {agent}: {e}", file=sys.stderr)
+50
View File
@@ -0,0 +1,50 @@
#!/home/node/.openclaw/mcp-venv/bin/python3
import sys, os, mimetypes, filetype
def get_file_info(filepath, mime_only=False):
if not os.path.exists(filepath):
return "cannot open (No such file or directory)", "cannot open (No such file or directory)"
if os.path.isdir(filepath):
return "directory", "inode/directory"
kind = filetype.guess(filepath)
if kind is not None:
return f"{kind.extension.upper()} image/media data ({kind.mime})", kind.mime
guessed_mime, _ = mimetypes.guess_type(filepath)
try:
with open(filepath, "rb") as f:
sample = f.read(4096)
if len(sample) == 0:
return "empty", "application/x-empty"
try:
sample.decode("utf-8")
if guessed_mime:
return f"{guessed_mime} text", guessed_mime
return "ASCII text", "text/plain"
except UnicodeDecodeError:
return "data", "application/octet-stream"
except Exception as e:
return f"cannot read ({e})", "application/octet-stream"
def main():
args = sys.argv[1:]
brief, mime_only, files = False, False, []
for arg in args:
if arg in ("-b", "--brief"):
brief = True
elif arg in ("-i", "--mime", "--mime-type"):
mime_only = True
elif arg.startswith("-") and len(arg) > 1 and not arg.startswith("--"):
if "b" in arg: brief = True
if "i" in arg: mime_only = True
else:
files.append(arg)
if not files:
sys.stderr.write("Usage: file [-b] [-i|--mime-type] <file>...\n")
sys.exit(1)
for f in files:
desc, mime = get_file_info(f, mime_only)
res = mime if mime_only else desc
print(res if brief else f"{f}: {res}")
if __name__ == "__main__":
main()
@@ -0,0 +1,78 @@
import fs from "node:fs";
import path from "node:path";
import { execSync, spawn } from "node:child_process";
if (!process.env.__OPENCLAW_PREFLIGHT_DONE) {
process.env.__OPENCLAW_PREFLIGHT_DONE = "1";
const isGateway = process.argv.some(arg => arg.endsWith("openclaw.mjs") || arg.endsWith("openclaw")) && process.argv.includes("gateway");
if (isGateway) {
const t0 = Date.now();
console.log("[gateway-preflight] Initializing container self-healing preflight...");
// 1. Clean up stale gateway lock files from ungraceful kills
for (const dir of ["/tmp/openclaw-1000", "/tmp"]) {
if (fs.existsSync(dir)) {
try {
const files = fs.readdirSync(dir);
for (const file of files) {
if (file.startsWith("gateway") && file.endsWith(".lock")) {
const fullPath = path.join(dir, file);
fs.unlinkSync(fullPath);
console.log(`[gateway-preflight] Removed stale lock: ${fullPath}`);
}
}
} catch (e) {
console.warn(`[gateway-preflight] Lock cleanup warning in ${dir}:`, e.message);
}
}
}
// 2. Clear false database quarantines
const qPath = "/home/node/.openclaw/state/openclaw-quarantine.sqlite";
if (fs.existsSync(qPath)) {
try {
execSync(`/home/node/.openclaw/.local/bin/python3 -c "import sqlite3; c=sqlite3.connect(\x27${qPath}\x27); c.execute(\x27DELETE FROM quarantined_databases\x27); c.commit(); c.close()"`, { stdio: "ignore" });
console.log("[gateway-preflight] Cleared database quarantine.");
} catch (_) {}
}
// 3. Deduplicate subagent vector databases (keeps non-main agent DBs < 15MB)
try {
execSync("/home/node/.openclaw/.local/bin/python3 /home/node/.openclaw/scripts/dedup_subagent_vectors.py", { stdio: "inherit" });
} catch (e) {
console.warn("[gateway-preflight] Vector deduplication warning:", e.message);
}
// 4. Pre-warm page cache sequentially (transforms random 70 IOPS disk reads into sequential streaming)
try {
const pw0 = Date.now();
execSync("cat /home/node/.openclaw/state/openclaw.sqlite /home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite > /dev/null 2>&1 || true");
console.log(`[gateway-preflight] Pre-warmed SQLite databases in ${Date.now() - pw0}ms`);
} catch (_) {}
// 5. Recover any wedged running sessions left over from ungraceful container restart
try {
execSync("/home/node/.openclaw/.local/bin/python3 /home/node/.openclaw/scripts/recover_sessions.py", { stdio: "inherit" });
} catch (e) {
console.warn("[gateway-preflight] Session recovery warning:", e.message);
}
// 6. Spawn background turn replay daemon if interrupted turns were detected
try {
if (fs.existsSync("/tmp/openclaw-interrupted-turns.json")) {
console.log("[gateway-preflight] Spawning background turn replay daemon...");
const child = spawn("/home/node/.openclaw/.local/bin/python3", ["/home/node/.openclaw/scripts/replay_interrupted_turns.py"], {
detached: true,
stdio: "ignore"
});
child.unref();
}
} catch (e) {
console.warn("[gateway-preflight] Could not spawn turn replay daemon:", e.message);
}
console.log(`[gateway-preflight] Self-healing completed in ${Date.now() - t0}ms.`);
}
}
+64
View File
@@ -0,0 +1,64 @@
import os
p = "/home/node/.openclaw/mcp-venv/lib/python3.12/site-packages/garmin_mcp/workouts.py"
if os.path.exists(p):
with open(p, "r") as f:
c = f.read()
if "_normalize_sport_type" not in c:
patch = '''
SPORT_TYPE_MAPPINGS = {
"running": {"sportTypeId": 1, "sportTypeKey": "running", "displayOrder": 1},
"cycling": {"sportTypeId": 2, "sportTypeKey": "cycling", "displayOrder": 2},
"other": {"sportTypeId": 3, "sportTypeKey": "other", "displayOrder": 3},
"lap_swimming": {"sportTypeId": 4, "sportTypeKey": "lap_swimming", "displayOrder": 4},
"swimming": {"sportTypeId": 4, "sportTypeKey": "lap_swimming", "displayOrder": 4},
"strength_training": {"sportTypeId": 5, "sportTypeKey": "strength_training", "displayOrder": 5},
"cardio_training": {"sportTypeId": 6, "sportTypeKey": "cardio_training", "displayOrder": 6},
"cardio": {"sportTypeId": 6, "sportTypeKey": "cardio_training", "displayOrder": 6},
"yoga": {"sportTypeId": 7, "sportTypeKey": "yoga", "displayOrder": 8},
"pilates": {"sportTypeId": 8, "sportTypeKey": "pilates", "displayOrder": 9},
"hiit": {"sportTypeId": 9, "sportTypeKey": "hiit", "displayOrder": 7},
"mobility": {"sportTypeId": 11, "sportTypeKey": "mobility", "displayOrder": 11},
"walking": {"sportTypeId": 12, "sportTypeKey": "walking", "displayOrder": 10},
"rucking": {"sportTypeId": 13, "sportTypeKey": "rucking", "displayOrder": 12},
}
SPORT_ID_MAPPINGS = {v["sportTypeId"]: v for v in SPORT_TYPE_MAPPINGS.values()}
def _resolve_sport_block(st):
if isinstance(st, str):
k = st.strip().lower()
if k in SPORT_TYPE_MAPPINGS:
return dict(SPORT_TYPE_MAPPINGS[k])
elif isinstance(st, dict):
key = st.get("sportTypeKey")
sid = st.get("sportTypeId")
if key and str(key).strip().lower() in SPORT_TYPE_MAPPINGS:
base = dict(SPORT_TYPE_MAPPINGS[str(key).strip().lower()])
if sid is not None and sid != 0 and sid != base["sportTypeId"]:
raise ValueError(f"sportType mismatch: {key} vs {sid}")
return base
if sid is not None and sid in SPORT_ID_MAPPINGS:
return dict(SPORT_ID_MAPPINGS[sid])
return None
def _normalize_sport_type(workout_data: dict) -> None:
resolved = _resolve_sport_block(workout_data.get("sportType"))
segments = workout_data.get("workoutSegments") or []
if not resolved and segments:
for seg in segments:
resolved = _resolve_sport_block(seg.get("sportType"))
if resolved:
break
if not resolved:
raise ValueError("Missing or invalid sportType. Both sportTypeId and sportTypeKey are required.")
workout_data["sportType"] = resolved
for seg in segments:
st = _resolve_sport_block(seg.get("sportType"))
seg["sportType"] = st if st else dict(resolved)
'''
t = "def _normalize_workout_steps(workout_data: dict) -> None:"
if t in c:
with open(p, "w") as f:
f.write(c.replace(t, patch + t + "\n _normalize_sport_type(workout_data)", 1))
print("Successfully patched garmin_mcp workouts.py!")
+61
View File
@@ -0,0 +1,61 @@
import fs from "node:fs";
import path from "node:path";
import { globSync } from "node:fs";
const baseDir = "/home/node/.openclaw/extensions/perplexity/dist";
if (fs.existsSync(baseDir)) {
const files = fs.readdirSync(baseDir).filter(f => f.startsWith("perplexity-web-search-provider.runtime-") && f.endsWith(".js"));
for (const filename of files) {
const file = path.join(baseDir, filename);
let code = fs.readFileSync(file, "utf8");
if (!code.includes("/* PATCHED_CHAT_COMPLETIONS */")) {
const target = "const structured = runtime.transport === \"search_api\";";
const fastPath = `const structured = runtime.transport === "search_api"; /* PATCHED_CHAT_COMPLETIONS */
if (!structured) {
const cacheKey = buildSearchCacheKey([
"perplexity",
runtime.transport,
runtime.baseUrl,
runtime.model,
query,
freshness
]);
const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
const cached = readCachedSearchPayload(cacheKey, cacheTtlMs);
if (cached) return cached;
const start = Date.now();
const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
const result = await runPerplexitySearch({
query,
apiKey: runtime.apiKey,
baseUrl: runtime.baseUrl,
model: runtime.model,
timeoutSeconds,
signal,
freshness
});
const payload = {
query,
provider: "perplexity",
model: runtime.model,
tookMs: Date.now() - start,
externalContent: {
untrusted: true,
source: "web_search",
provider: "perplexity",
wrapped: true
},
content: wrapWebContent(result.content, "web_search"),
citations: result.citations
};
writeCachedSearchPayload(cacheKey, payload);
return payload;
}`;
if (code.includes(target)) {
code = code.replace(target, fastPath);
fs.writeFileSync(file, code, "utf8");
console.log("Patched perplexity runtime for chat completions:", file);
}
}
}
}
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
import glob, sqlite3, json, os, sys, time
t0 = time.time()
recovered = 0
interrupted_turns = []
def extract_prompt(msg):
if not msg: return ""
content = msg.get("content")
if isinstance(content, str): return content
if isinstance(content, list):
parts = []
for c in content:
if isinstance(c, dict): parts.append(c.get("text") or c.get("content") or "")
elif isinstance(c, str): parts.append(c)
return "\n".join(filter(None, parts))
return str(content or "")
for db in sorted(glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")):
try:
conn = sqlite3.connect(db, timeout=10.0)
cursor = conn.cursor()
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,))
cursor.execute("SELECT session_key, entry_json, current_session_id FROM session_nodes WHERE status = \"running\"")
rows = cursor.fetchall()
agent_id = os.path.basename(os.path.dirname(os.path.dirname(db)))
for sk, ej, csid in rows:
try:
p = json.loads(ej) if ej else {}
sid = csid or p.get("sessionId")
if sid:
ev = cursor.execute("SELECT seq, event_json FROM transcript_events WHERE session_id = ? ORDER BY seq DESC LIMIT 1", (sid,)).fetchone()
if ev:
ev_seq, ev_raw = ev
try:
ev_data = json.loads(ev_raw)
msg = ev_data.get("message") or ev_data
if msg.get("role") == "user":
prompt_text = extract_prompt(msg)
ctx = p.get("restartRecoveryDeliveryContext") or p.get("delivery", {}).get("context") or p.get("delivery", {}).get("route", {}).get("context") or {}
interrupted_turns.append({
"agent": agent_id,
"session_key": sk,
"session_id": sid,
"context": ctx,
"prompt": prompt_text
})
cursor.execute("PRAGMA foreign_keys = ON")
cursor.execute("DELETE FROM transcript_events WHERE session_id = ? AND seq = ?", (sid, ev_seq))
except Exception as ex:
print(f"[recover_sessions] Error parsing event: {ex}", file=sys.stderr)
p["status"] = "done"
p["activeWriterRunId"] = None
p["activeClaimRunId"] = None
p["restartRecoveryDeliveryRunId"] = None
cursor.execute("UPDATE session_nodes SET status = \"done\", entry_json = ? WHERE session_key = ?", (json.dumps(p), sk))
cursor.execute("UPDATE session_nodes SET entry_valid = 1 WHERE session_key = ?", (sk,))
recovered += 1
except Exception: pass
cursor.execute("UPDATE session_windows SET status = \"done\" WHERE status = \"running\"")
conn.commit()
conn.close()
except Exception as e:
print(f"[recover_sessions] Warning for {db}: {e}", file=sys.stderr)
if interrupted_turns:
try:
with open("/tmp/openclaw-interrupted-turns.json", "w") as f:
json.dump(interrupted_turns, f, indent=2)
print(f"[recover_sessions] Detected {len(interrupted_turns)} interrupted user turn(s) saved to /tmp/openclaw-interrupted-turns.json")
except Exception as e:
print(f"[recover_sessions] Error writing interrupted turns: {e}", file=sys.stderr)
print(f"[recover_sessions] Completed in {time.time() - t0:.2f}s ({recovered} wedged sessions recovered)")
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
import json, os, subprocess, sys, time, urllib.request
INTERRUPTED_FILE = "/tmp/openclaw-interrupted-turns.json"
if not os.path.exists(INTERRUPTED_FILE):
sys.exit(0)
try:
with open(INTERRUPTED_FILE, "r") as f:
turns = json.load(f)
except Exception as e:
print(f"[replay_interrupted_turns] Failed to read {INTERRUPTED_FILE}: {e}", file=sys.stderr)
sys.exit(0)
if not turns:
sys.exit(0)
print(f"[replay_interrupted_turns] Found {len(turns)} interrupted turn(s). Waiting for gateway /healthz...")
gateway_ready = False
for _ in range(60):
try:
with urllib.request.urlopen("http://127.0.0.1:18790/healthz", timeout=3) as resp:
if resp.status == 200:
gateway_ready = True
break
except Exception:
pass
time.sleep(2)
if not gateway_ready:
print("[replay_interrupted_turns] Gateway did not become healthy in 120s. Aborting.", file=sys.stderr)
sys.exit(1)
# Allow 5s for Telegram ingress to bind
time.sleep(5)
for turn in turns:
sk = turn.get("session_key")
sid = turn.get("session_id")
agent_id = turn.get("agent")
ctx = turn.get("context") or {}
prompt = turn.get("prompt") or ""
channel = ctx.get("channel") or "telegram"
to_target = ctx.get("to") or ""
thread_id = ctx.get("threadId")
target = to_target
if target.startswith("telegram:"):
parts = target.split(":")
target = parts[1] if len(parts) > 1 else target
print(f"[replay_interrupted_turns] Processing agent '{agent_id}' on session '{sk}' target='{target}' thread='{thread_id}'")
# 1. Send status notification to Telegram if target exists
if target:
notice = "⚠️ OpenClaw wurde während der Bearbeitung neu gestartet – deine Anfrage wird jetzt nachgeholt..."
notify_cmd = [
"openclaw", "message", "send",
"--channel", channel,
"--target", str(target),
"-m", notice
]
if thread_id is not None:
notify_cmd.extend(["--thread-id", str(thread_id)])
try:
subprocess.run(notify_cmd, check=False, timeout=30)
print(f"[replay_interrupted_turns] Sent recovery notice to {target}")
except Exception as e:
print(f"[replay_interrupted_turns] Notice failed: {e}", file=sys.stderr)
# 2. Replay agent turn
if sk and prompt:
p_short = (prompt[:60] + "...") if len(prompt) > 60 else prompt
print(f"[replay_interrupted_turns] Replaying agent turn: {p_short}")
agent_cmd = [
"openclaw", "agent",
"--session-key", sk,
"-m", prompt,
"--deliver"
]
try:
res = subprocess.run(agent_cmd, capture_output=True, text=True, timeout=300)
if res.returncode == 0:
print(f"[replay_interrupted_turns] Turn replay succeeded for {sk}")
else:
print(f"[replay_interrupted_turns] Turn replay exited with code {res.returncode}: {res.stderr[:200]}", file=sys.stderr)
except Exception as e:
print(f"[replay_interrupted_turns] Replay execution error: {e}", file=sys.stderr)
try:
os.remove(INTERRUPTED_FILE)
print(f"[replay_interrupted_turns] Cleaned up {INTERRUPTED_FILE}")
except Exception:
pass
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
sync_crons.py
Idempotently reconciles declarative OpenClaw automations defined in crons.yaml
with the running OpenClaw gateway state.
Can run directly inside the container or remotely via kubectl exec.
"""
import json
import os
import subprocess
import sys
import yaml
def find_crons_file():
candidates = [
os.environ.get("CRONS_FILE"),
"crons.yaml",
"k8s/openclaw/crons.yaml",
"/home/node/.openclaw/scripts/crons.yaml",
os.path.join(os.path.dirname(__file__), "../crons.yaml"),
os.path.join(os.path.dirname(__file__), "crons.yaml"),
]
for c in candidates:
if c and os.path.exists(c):
return os.path.abspath(c)
raise FileNotFoundError("Could not find crons.yaml. Pass via CRONS_FILE environment variable.")
def is_inside_container():
return os.path.exists("/home/node/.openclaw") or os.environ.get("OPENCLAW_HOME") is not None
def run_openclaw_cmd(args):
if is_inside_container():
cmd = ["openclaw"] + args
else:
namespace = os.environ.get("NAMESPACE", "openclaw")
pod = os.environ.get("POD_NAME", "openclaw-0")
container = os.environ.get("CONTAINER_NAME", "openclaw")
cmd = ["kubectl", "exec", "-n", namespace, pod, "-c", container, "--", "openclaw"] + args
res = subprocess.run(cmd, capture_output=True, text=True)
if res.returncode != 0:
raise RuntimeError(f"OpenClaw CLI command failed ({' '.join(cmd)}):\n{res.stderr}\n{res.stdout}")
return res.stdout
def list_existing_jobs():
raw = run_openclaw_cmd(["cron", "list", "--json"])
try:
data = json.loads(raw)
return data.get("jobs", [])
except Exception as e:
print(f"Warning: could not parse cron list json: {e}\nRaw output: {raw[:300]}", file=sys.stderr)
return []
def reconcile_jobs():
file_path = find_crons_file()
print(f"=== Syncing OpenClaw Crons-as-Code from {file_path} ===")
with open(file_path, "r", encoding="utf-8") as f:
spec = yaml.safe_load(f)
declarations = spec.get("jobs", [])
existing_jobs = list_existing_jobs()
print(f"Found {len(declarations)} declared jobs in YAML, {len(existing_jobs)} active jobs on gateway.")
for job in declarations:
name = job.get("name")
decl_key = job.get("declarationKey") or name
desc = job.get("description", "")
sched = job.get("schedule", {})
# Match existing job by declarationKey or exact name
match = next((j for j in existing_jobs if j.get("declarationKey") == decl_key or j.get("name") == name), None)
if match:
existing_id = match.get("id")
print(f"Removing existing job '{name}' (ID {existing_id}) for idempotent recreation...")
try:
run_openclaw_cmd(["cron", "rm", existing_id])
except Exception as e:
print(f"Warning removing {existing_id}: {e}")
# Build add command
cmd = [
"cron", "add",
"--name", name,
"--declaration-key", decl_key,
"--description", desc,
]
if sched.get("kind") == "every":
cmd.extend(["--every", sched.get("every", "15m")])
elif sched.get("kind") == "cron":
cmd.extend(["--cron", sched.get("expr", "* * * * *")])
if sched.get("tz"):
cmd.extend(["--tz", sched.get("tz")])
if job.get("session"):
cmd.extend(["--session", job.get("session")])
if job.get("delivery") == "none":
cmd.append("--no-deliver")
if job.get("command"):
cmd.extend(["--command", job.get("command")])
if job.get("agent"):
cmd.extend(["--agent", job.get("agent")])
if job.get("model"):
cmd.extend(["--model", job.get("model")])
if job.get("channel"):
cmd.extend(["--channel", job.get("channel")])
if job.get("to"):
cmd.extend(["--to", job.get("to")])
if job.get("threadId"):
cmd.extend(["--thread-id", str(job.get("threadId"))])
if job.get("announce"):
cmd.append("--announce")
if job.get("message"):
cmd.extend(["--message", job.get("message")])
print(f"Registering job '{name}'...")
run_openclaw_cmd(cmd)
print(f"Successfully registered '{name}'.")
print("=== OpenClaw Crons-as-Code reconciliation complete! ===")
if __name__ == "__main__":
reconcile_jobs()