diff --git a/.gitignore b/.gitignore index bb6ea77..d4b486d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,10 @@ k8s/gitea-chart/ .vagrant k8s/tekton-pipelines/auth .DS_Store +node_modules/ +**/node_modules/ +dist/ +**/dist/ +*.tsbuildinfo +.venv/ +**/__pycache__/ diff --git a/k8s/openclaw/AGENTS.md b/k8s/openclaw/AGENTS.md index 183b034..7b600e2 100644 --- a/k8s/openclaw/AGENTS.md +++ b/k8s/openclaw/AGENTS.md @@ -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 diff --git a/k8s/openclaw/configmap-scripts.yaml b/k8s/openclaw/configmap-scripts.yaml new file mode 100644 index 0000000..5bfe25e --- /dev/null +++ b/k8s/openclaw/configmap-scripts.yaml @@ -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]\ + \ ...\\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}] \")\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 | --agent ) [--limit ]\")\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 ; session . 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" diff --git a/k8s/openclaw/cronjob-wiki-backup.secret.yaml b/k8s/openclaw/cronjob-wiki-backup.secret.yaml index 663b255..9f223f2 100644 Binary files a/k8s/openclaw/cronjob-wiki-backup.secret.yaml and b/k8s/openclaw/cronjob-wiki-backup.secret.yaml differ diff --git a/k8s/openclaw/crons.yaml b/k8s/openclaw/crons.yaml new file mode 100644 index 0000000..886d9b8 --- /dev/null +++ b/k8s/openclaw/crons.yaml @@ -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 ; session . 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. diff --git a/k8s/openclaw/openclaw-instance.secret.yaml b/k8s/openclaw/openclaw-instance.secret.yaml index 0ebdcf7..2a64040 100644 Binary files a/k8s/openclaw/openclaw-instance.secret.yaml and b/k8s/openclaw/openclaw-instance.secret.yaml differ diff --git a/k8s/openclaw/scripts/bootstrap-deps.sh b/k8s/openclaw/scripts/bootstrap-deps.sh new file mode 100644 index 0000000..4405102 --- /dev/null +++ b/k8s/openclaw/scripts/bootstrap-deps.sh @@ -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 ===" diff --git a/k8s/openclaw/scripts/dedup_subagent_vectors.py b/k8s/openclaw/scripts/dedup_subagent_vectors.py new file mode 100644 index 0000000..b4752ac --- /dev/null +++ b/k8s/openclaw/scripts/dedup_subagent_vectors.py @@ -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) diff --git a/k8s/openclaw/scripts/file_cli.py b/k8s/openclaw/scripts/file_cli.py new file mode 100644 index 0000000..cb1c199 --- /dev/null +++ b/k8s/openclaw/scripts/file_cli.py @@ -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] ...\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() diff --git a/k8s/openclaw/scripts/gateway-preflight.mjs b/k8s/openclaw/scripts/gateway-preflight.mjs new file mode 100644 index 0000000..0c6b2f9 --- /dev/null +++ b/k8s/openclaw/scripts/gateway-preflight.mjs @@ -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.`); + } + } diff --git a/k8s/openclaw/scripts/patch_garmin_mcp.py b/k8s/openclaw/scripts/patch_garmin_mcp.py new file mode 100644 index 0000000..eab89f7 --- /dev/null +++ b/k8s/openclaw/scripts/patch_garmin_mcp.py @@ -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!") diff --git a/k8s/openclaw/scripts/patch_perplexity.mjs b/k8s/openclaw/scripts/patch_perplexity.mjs new file mode 100644 index 0000000..102a823 --- /dev/null +++ b/k8s/openclaw/scripts/patch_perplexity.mjs @@ -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); + } + } + } +} diff --git a/k8s/openclaw/scripts/recover_sessions.py b/k8s/openclaw/scripts/recover_sessions.py new file mode 100644 index 0000000..8db7cc6 --- /dev/null +++ b/k8s/openclaw/scripts/recover_sessions.py @@ -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)") diff --git a/k8s/openclaw/scripts/replay_interrupted_turns.py b/k8s/openclaw/scripts/replay_interrupted_turns.py new file mode 100644 index 0000000..47f5e0a --- /dev/null +++ b/k8s/openclaw/scripts/replay_interrupted_turns.py @@ -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 diff --git a/k8s/openclaw/scripts/sync_crons.py b/k8s/openclaw/scripts/sync_crons.py new file mode 100644 index 0000000..fbf6f71 --- /dev/null +++ b/k8s/openclaw/scripts/sync_crons.py @@ -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() diff --git a/knowledge-wiki b/knowledge-wiki index 39074f8..1008b2c 160000 --- a/knowledge-wiki +++ b/knowledge-wiki @@ -1 +1 @@ -Subproject commit 39074f81d9b54c68fcbcf908408a3af58ccf5568 +Subproject commit 1008b2c18dd1c1e4a7ff4bd6353fe5756a8cd8db