feat(openclaw): modularize scripts, implement crons-as-code and EaC governance
This commit is contained in:
@@ -2,3 +2,10 @@ k8s/gitea-chart/
|
||||
.vagrant
|
||||
k8s/tekton-pipelines/auth
|
||||
.DS_Store
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
dist/
|
||||
**/dist/
|
||||
*.tsbuildinfo
|
||||
.venv/
|
||||
**/__pycache__/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
@@ -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.
@@ -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)
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
@@ -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!")
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
+1
-1
Submodule knowledge-wiki updated: 39074f81d9...1008b2c18d
Reference in New Issue
Block a user