From dd60a095c66243c405a75603afb571df5142627c Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Sun, 18 Oct 2026 10:54:59 +0200 Subject: [PATCH] feat(openclaw): upgrade fleet to DeepSeek V4.1 Flash 1017 and harden operations - Upgrade OpenClaw primary model from deepseek-v4.1-flash to deepseek-v4.1-flash-1017 across all interactive agents, heartbeats, compaction, and cron jobs. - Configure hardened OpenRouter provider routing for 1017 (deepinfra, inference-net, parasail, cloudflare) with strict fp8/bf16 quantization and tool_choice verification. - Retain deepseek-v4.1-flash (0910), z-ai/glm-5.3-flash, and qwen3.8-flash in the automated fallback chain. - Update ADR 0002 with 1017 economics (30% price reduction on prompt/cache, +60% intelligence benchmark increase) and a 7-point provider qualification playbook. - Add operational scripts and tooling for session compaction, subagent vector deduplication, gateway preflight, and triage diagnostics in k8s/openclaw. - Update DuckDB backend engine, git-sync logic, and test suites. - Enhance Mila tracking UI components and add container build configuration. - Sync Terraform infrastructure state for Authentik. --- ...w-model-selection-tiering-and-economics.md | 143 +- k8s/openclaw/TROUBLESHOOTING.md | 45 + k8s/openclaw/configmap-scripts.yaml | 1530 +++++++---------- k8s/openclaw/crons.yaml | 6 +- k8s/openclaw/openclaw-instance.secret.yaml | Bin 57205 -> 62837 bytes k8s/openclaw/scripts/compact_sessions.py | 6 +- .../scripts/dedup_subagent_vectors.py | 80 +- k8s/openclaw/scripts/gateway-preflight.mjs | 144 +- k8s/openclaw/scripts/openclaw-triage.py | 2 +- k8s/openclaw/scripts/recover_sessions.py | 166 +- .../scripts/replay_interrupted_turns.py | 170 +- k8s/openclaw/setup-garmin-cron.sh | 2 +- k8s/openclaw/setup-itguy-cron.sh | 2 +- knowledge-wiki | 2 +- projects/duckdb-backend/app/engine.py | 173 +- projects/duckdb-backend/app/git_sync.py | 8 +- projects/duckdb-backend/app/main.py | 33 +- projects/duckdb-backend/tests/conftest.py | 11 +- .../duckdb-backend/tests/test_api_events.py | 47 + projects/mila/.dockerignore | 4 + projects/mila/src/App.vue | 72 +- projects/mila/src/components/SleepChart.vue | 96 +- projects/mila/src/types.ts | 2 + terraform/infrastructure/authentik.tf | 30 + terraform/infrastructure/terraform.tfstate | 156 +- .../infrastructure/terraform.tfstate.backup | 157 +- 26 files changed, 1786 insertions(+), 1301 deletions(-) create mode 100644 projects/mila/.dockerignore diff --git a/docs/adr/0002-openclaw-model-selection-tiering-and-economics.md b/docs/adr/0002-openclaw-model-selection-tiering-and-economics.md index 5e302a4..f5a41c7 100644 --- a/docs/adr/0002-openclaw-model-selection-tiering-and-economics.md +++ b/docs/adr/0002-openclaw-model-selection-tiering-and-economics.md @@ -1,5 +1,5 @@ --- -status: "accepted (trial: GLM-5.3-Flash)" +status: "accepted (fleet-wide primary: DeepSeek-V4.1-Flash-1017; updated 2026-10-17)" date: 2026-09-13 decision-makers: ["Moritz Graf", "Antigravity"] consulted: [] @@ -28,7 +28,7 @@ How do we architect the full-spectrum model selection across **all modalities** ## Decision Drivers -* **State-of-the-Art Recency**: Exclusively employ the latest generation models (e.g. Gemini 3.1, GLM 5.3, Whisper V3 Turbo, Sonar Pro). Completely decommission legacy generations (e.g. Gemini 2.5). +* **State-of-the-Art Recency**: Exclusively employ the latest generation models (e.g. DeepSeek V4.1 Flash 1017, Gemini 3.8/3.1, GLM 5.3, Whisper V3 Turbo, Sonar Pro). Completely decommission legacy generations (e.g. Gemini 2.5). * **Conversational & Reasoning Quality**: High emotional intelligence, nuanced German fluency, personal context recall, and strict adherence to tool-calling protocols (ADR 0001). * **Strict Modality Decoupling**: Each capability must route to its dedicated, best-in-class engine: - Text & Agentic Reasoning @@ -38,9 +38,9 @@ How do we architect the full-spectrum model selection across **all modalities** - Text-to-Speech / Audio Output (synthesizing briefings and podcasts) - Web Search & Retrieval (grounded search) - Semantic Embeddings (vector memory) -* **Inference Economics**: Low per-token costs for large conversational contexts (15k–80k tokens), zero background bleed, and predictable billing. +* **Inference Economics**: Low per-token costs for large conversational contexts (15k–80k tokens), high prompt-cache discount efficiency (`cacheRead`), zero background bleed, and predictable billing. * **Latency & Turn Responsiveness**: Conversational turns on Telegram must return within **4–10 seconds**, avoiding provider queuing or massive prefill bottlenecks. -* **Resilience & Fault Tolerance**: Automated fallback chains across independent providers on OpenRouter. +* **Resilience & Fault Tolerance**: Automated fallback chains and strict quantization/provider pins (`fp8`/`bf16`, `tool_choice` support) across independent providers on OpenRouter. --- @@ -60,22 +60,23 @@ How do we architect the full-spectrum model selection across **all modalities** │ Tier 1 │ │ Tier 2 │ │ Tier 3 │ │ Tier 4 │ │ Tier 5 │ │ • Low Latency │ │ • Deep Analysis │ │ • target: none │ │ • Nano Banana 2 │ │ • Whisper V3 │ │ • Tool Routing │ │ • Autonomous │ │ • Zero Bleed │ │ • Dedicated │ │ • Gemini 3.1 │ -│ • Native Vision │ │ • Isolated Run │ │ • 30m Polling │ │ Raster Head │ │ Flash TTS │ +│ • Decoupled OCR │ │ • Isolated Run │ │ • 30m Polling │ │ Raster Head │ │ Flash TTS │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Primary (Trial):│ │ Primary (Trial):│ │ Primary: │ │ Primary: │ │ STT: │ -│ z-ai/ │ │ z-ai/ │ │ z-ai/ │ │ google/ │ │ openai/ │ -│ glm-5.3-flash │ │ glm-5.3-flash │ │ glm-5.3-flash │ │ gemini-3.1- │ │ whisper-large- │ -│ │ │ │ │ │ │ flash-image │ │ v3-turbo │ -│ Fallbacks: │ │ Fallbacks: │ │ Fallbacks: │ │ │ │ -│ 1. deepseek- │ │ 1. deepseek- │ │ 1. deepseek- │ │ Fallbacks: │ │ TTS: │ -│ v4.1-flash │ │ v4.1-flash │ │ v4.1-flash │ │ 1. gemini-3.1- │ │ google/ │ -│ 2. qwen3.8-flash│ │ 2. qwen3.8-flash│ │ 2. qwen3.8-flash│ │ flash-preview│ │ gemini-3.1- │ -│ │ │ │ │ │ │ 2. gemini-3-pro-│ │ flash-tts │ -│ Vision Fallback:│ │ │ │ │ │ image │ │ (FB: MiniMax 2.8│ -│ google/ │ │ │ │ │ │ │ │ Speech HD) │ +│ Primary: │ │ Primary: │ │ Primary: │ │ Primary: │ │ STT: │ +│ deepseek/ │ │ deepseek/ │ │ deepseek/ │ │ google/ │ │ openai/ │ +│ deepseek-v4.1- │ │ deepseek-v4.1- │ │ deepseek-v4.1- │ │ gemini-3.1- │ │ whisper-large- │ +│ flash-1017 │ │ flash-1017 │ │ flash-1017 │ │ flash-image │ │ v3-turbo │ +│ │ │ │ │ │ │ │ │ +│ Fallbacks: │ │ Fallbacks: │ │ Fallbacks: │ │ Fallbacks: │ │ TTS: │ +│ 1. v4.1-flash │ │ 1. v4.1-flash │ │ 1. v4.1-flash │ │ 1. gemini-3.1- │ │ google/ │ +│ 2. glm-5.3-flash│ │ 2. glm-5.3-flash│ │ 2. glm-5.3-flash│ │ flash-preview│ │ gemini-3.1- │ +│ 3. qwen3.8-flash│ │ 3. qwen3.8-flash│ │ 3. qwen3.8-flash│ │ 2. gemini-3-pro-│ │ flash-tts │ +│ │ │ │ │ │ │ image │ │ (FB: MiniMax 2.8│ +│ Vision (OCR): │ │ │ │ │ │ │ │ Speech HD) │ +│ google/ │ │ │ │ │ │ │ │ │ │ gemini-3.8- │ │ │ │ │ │ │ │ │ │ flash │ │ │ │ │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ @@ -86,27 +87,28 @@ How do we architect the full-spectrum model selection across **all modalities** ## Modality Specifications & Cost-Benefit Analysis ### 1. Interactive Conductor & Agentic Reasoning (Tier 1) -* **Primary (Active Trial)**: `openrouter/z-ai/glm-5.3-flash` - - **Pricing**: \$0.075 / 1M prompt tokens; \$0.300 / 1M completion tokens. This is 50–75% more cost-effective than DeepSeek V4.1-Flash and offers a 1.3M token context window. Under daytime context headroom (~25k–60k tokens), daily cost is only **~\$0.01–\$0.02 / day**. - - **Performance & Reasoning**: SOTA Chinese & English reasoning, competitive coding benchmarks, and native vision perception. - - **Forensic Diagnosis & Tool-Calling Protocol Fix**: - - *Incident*: On September 14, 2026, GLM-5.3-Flash emitted raw `supermarket__supermarket_search...` XML into Telegram instead of executing tools. - - *Root Cause Identified*: In Zhipu AI's pre-training tokenizer, function calls are emitted as special XML tokens. When models are queried through OpenRouter's OpenAI-compatible API, OpenRouter only intercepts and translates `` tokens into JSON if the tool `name` exists in the request's registered `tools: [...]` array. OpenClaw uses dynamic Tool Search (`tools.toolSearch.mode: "directory"`) where only 3 control tools (`tool_search`, `tool_describe`, `tool_call`) are registered in `tools`. When GLM received deferred schemas (e.g., `supermarket__supermarket_search`), its pre-training prior prompted it to call the tool directly. Because `supermarket__supermarket_search` was not in OpenRouter's `tools` array, OpenRouter passed the raw XML through as message text. Once that text entered the session history (events SEQ 297 & 306), GLM was in-context primed to repeat the XML pattern. - - *Remediation & Verification*: We introduced an explicit deferred tool execution protocol into `AGENTS.md` instructing the model that deferred tools must NEVER be called directly and MUST be routed via `tool_call({ id: 'tool_name', args: { ... } })`. Direct OpenRouter API probes confirmed that with this rule, GLM-5.3-Flash calls `tool_call` with 100% compliant JSON and zero XML leakage. Session events were sanitized. +* **Primary (Fleet-Wide Standard as of 2026-10-17)**: `openrouter/deepseek/deepseek-v4.1-flash-1017` + - **1017 Snapshot Upgrade (Pareto Jump in Quality & Price)**: Released on October 17, 2026, `deepseek/deepseek-v4.1-flash-1017` boosts the Artificial Analysis Intelligence Index from **24.7 to 39.5** (+60%) via Reinforcement Learning with Self-Feedback (RLSF) while cutting DeepInfra `fp8` pricing by an additional **30%** (`$0.098/M` prompt, `$0.294/M` completion, and **`$0.00294/M` `cacheRead`** vs. `$0.140/$0.420/$0.0042` on the 0910 baseline). + - **Provider Pinning & Tool-Compatibility Verification**: + - Pinned `order: ["deepinfra", "inference-net", "parasail", "cloudflare"]`, `quantizations: ["fp8", "bf16", "unknown"]`, and `ignore: ["morph", "baseten", "deepseek", "open-inference", "decart", "sail-research", "wafer"]`. + - Live OpenRouter routing forensics verified that `morph/fp8` is filtered out by OpenRouter's `Filter by Tool Compatibility` when `tools` are passed, `deepseek` (official) is filtered by account privacy guardrails (`Paid model training violation`), and `baseten` returns `HTTP 429`. Meanwhile, `deepinfra`, `inference-net`, `parasail`, and `cloudflare` deliver 100% tool-calling reliability and 2–5s latency. + - **Post-Trial Evaluation of `z-ai/glm-5.3-flash` (2026-09-13 to 2026-10-17)**: + - While `z-ai/glm-5.3-flash` was trialed as primary conductor, 17-day production telemetry (Oct 1–17, 2026) revealed **12 mid-stream tool-call aborts** (`stopReason: "error"`, `errorMessage: "Provider returned an incomplete or malformed tool call"`) across 5 agents (`main`, `grabengasse_media`, `koch`, `hebamme`, `doktor`) when generating nested `tool_call({ id, args })` payloads after `tool_describe`, as well as occasional thinking-only whitespace stalls (Issue 23) and long-context attention drift (Issues 22 & 25). + - Promoting `deepseek/deepseek-v4.1-flash-1017` to primary across all agents eliminates malformed tool-call stream aborts AND lowers total monthly cost by >50% compared to GLM 5.3 Flash. + - **Decoupled Vision Override**: `deepseek/deepseek-v4.1-flash-1017`, `deepseek/deepseek-v4.1-flash`, and `z-ai/glm-5.3-flash` are registered with `input: ["text"]` under `models.providers.openrouter.models` so that OpenClaw always routes user photos through `tools.media.image` (`google/gemini-3.8-flash`) for high-precision German OCR before handing enriched text to the chat model (Issue 29). * **Fallbacks**: - 1. `openrouter/deepseek/deepseek-v4.1-flash`: Rock-solid JSON tool-calling and sub-3s response times via dedicated OpenRouter ASIC cluster, available immediately if GLM experiences any upstream provider queuing or issues. - 2. `openrouter/qwen/qwen3.8-flash`: Provider diversity. + 1. `openrouter/deepseek/deepseek-v4.1-flash` (`0910` baseline across 25+ providers, pinned to `deepinfra`, `streamlake`, `inference-net`, `digitalocean`, `venice`, `nebius`). + 2. `openrouter/z-ai/glm-5.3-flash`: Secondary fallback pinned to `fp8`/`bf16` providers (`morph`, `near-ai`, `digitalocean`, `venice`, `streamlake`). + 3. `openrouter/qwen/qwen3.8-flash`: Provider diversity. ### 2. Autonomous Subagents (Tier 2) -* **General Subagents**: `openrouter/z-ai/glm-5.3-flash` - - **Execution**: Spawned via `sessions_spawn` with a 300-second timeout. Inherits the `tool_call` protocol guardrails from `AGENTS.md`. -* **Deep Analysis & IT Specialists (`itguy`, `recherche`)**: `openrouter/deepseek/deepseek-v4.1-flash` - - High-precision reasoning, robust nested tool loops without syntax drift, and immune to XML leakage on extensive research queries or CLI diagnostics. -* **Fallback**: `openrouter/deepseek/deepseek-v4.1-flash` / `openrouter/qwen/qwen3.8-flash`. +* **All Subagents & Specialists**: `openrouter/deepseek/deepseek-v4.1-flash-1017` + - **Execution**: Spawned via `sessions_spawn` with a 300-second timeout. Provides high-precision reasoning, rock-solid nested JSON tool loops (`tool_search` -> `tool_describe` -> `tool_call`), and zero syntax drift. +* **Fallback**: `openrouter/deepseek/deepseek-v4.1-flash` / `openrouter/z-ai/glm-5.3-flash` / `openrouter/qwen/qwen3.8-flash`. ### 3. Background Daemon & Heartbeats (Tier 3) -* **Configuration**: `heartbeat.model: "openrouter/z-ai/glm-5.3-flash"`, `heartbeat.target: none`, `heartbeat.lightContext: true`. -* **Rationale**: Decoupled completely from user conversation transcripts. Polling occurs against an isolated, minimal prompt (~400 tokens), dropping weekly heartbeat token burn from 21,000,000 to <150,000 tokens (<\$0.02 / week). +* **Configuration**: `heartbeat.model: "openrouter/deepseek/deepseek-v4.1-flash-1017"`, `heartbeat.target: none`, `heartbeat.lightContext: true`. +* **Rationale**: Because background heartbeats have a 75%+ `cacheRead` share (`22.86M` `cacheRead` vs. `7.73M` uncached input over 17 days), DeepSeek V4.1 Flash 1017's \$0.00294/M cache read rate on DeepInfra reduces 17-day heartbeat cost to <\$0.90 while keeping prompt cache warm on the primary provider. ### 4. Vision Input vs. Text-to-Image Generation (Tier 4) * **Crucial Distinction**: @@ -171,14 +173,83 @@ To align on remaining edge cases before final sign-off: --- +## OpenRouter Provider Qualification Criteria & Re-Evaluation Playbook + +Whenever a new model snapshot (such as `deepseek/deepseek-v4.1-flash-1017`) is evaluated or re-checked as new hosting providers join OpenRouter, every candidate provider MUST pass the following **7-point qualification gate** before being added to `extra_body.provider.order` in [openclaw-instance.secret.yaml](file:///Users/moritz/src/infrapuzzle/k8s/openclaw/openclaw-instance.secret.yaml): + +### 1. Hard Technical Gates (Must Pass All) +1. **Quantization Precision (`fp8` / `bf16` Only)**: + - **Rule**: Must run `fp8` or `bf16` (or verified full-precision `unknown` from trusted cloud hosts like Cloudflare). + - **Reject**: Any `fp4`, `nvfp4`, `mxfp4`, or `int4` quantization (`decart`, `sail-research`, `wafer`, `open-inference`, `phala` fp4). 4-bit quantizations severely degrade nested JSON tool-call syntax and German grammar. +2. **OpenRouter Routing Funnel & Guardrail Compatibility (`tools` + `tool_choice: "auto"` Pass)**: + - **Rule**: Must not be stripped by OpenRouter's internal routing filters (`Filter by Tool Compatibility` or `Filter by Guardrails`) when a request includes `tools: [...]` and `tool_choice: "auto"`. + - **Why**: Metadata in `/api/v1/models/.../endpoints` can claim `supports_tool_choice: true` even when OpenRouter's live router blocks the provider on actual tool calls (e.g. `morph/fp8` is stripped by `Filter by Tool Compatibility`, and `deepseek` official is stripped by `Paid model training violation` account privacy settings). +3. **Nested MCP `tool_call` Serialization Integrity (2/2 Live Pass)**: + - **Rule**: Must cleanly emit `finish_reason: "tool_calls"` with valid, parseable nested JSON arguments (`{"id": "mcp:...", "args": {...}}`) across back-to-back test runs without mid-stream SSE aborts (`Provider returned an incomplete or malformed tool call`) or HTTP `429` upstream rate limits. +4. **Context Window & Completion Headroom**: + - **Rule**: `context_length >= 262,144` (1,048,576 preferred) and `max_completion_tokens >= 131,072`. + - **Why**: Active daytime sessions with system prompts and tool schemas reach 20k–80k tokens. +5. **Uptime & Reliability**: + - **Rule**: `uptime_last_1d >= 95%` and `uptime_last_30m == 100%`. + +### 2. Economic & Latency Ranking (How to Order Passing Providers) +Among providers passing all 5 technical gates, rank them in `extra_body.provider.order` by **effective production cost** and **turn latency**: +1. **Implicit Prompt Cache Read Rate (`input_cache_read`) — Highest Weight**: + - Because **73%–94%** of OpenClaw's token volume (both interactive multi-turn chats and 30m background heartbeats) consists of cached prompt reads (`cacheRead`), a provider with `supports_implicit_caching: true` and a **95%–98% cache discount** (`<= $0.005 / 1M` tokens, e.g. `deepinfra` at `$0.00294/M` or `streamlake` at `$0.0030/M`) is vastly cheaper in net monthly spend than a provider with slightly cheaper uncached input but weak or missing `cacheRead` discounts (e.g. `nebius` with `cache_read: null` or `modal`/`primeintellect` at `$0.030/M`). +2. **Base Prompt & Completion Pricing**: + - Target `prompt <= $0.18 / 1M` and `completion <= $0.65 / 1M` for top-priority slots; allow higher-priced providers (`<= $0.30 / $1.20`) only in lower fallback slots (#3/#4) for outage resilience. +3. **End-to-End Tool-Call Latency**: + - Target **1.0s – 5.5s** for a ~300-token reasoning + tool-call turn. + +### 3. One-Shot Live Verification Command (For Future Checks) +Any agent (including a fast/lightweight model) can audit all currently available OpenRouter endpoints for `deepseek/deepseek-v4.1-flash-1017` in one step by running: + +```bash +# 1. Inspect current endpoint metadata, pricing, cache_read, and 24h uptime: +curl -s https://openrouter.ai/api/v1/models/deepseek/deepseek-v4.1-flash-1017/endpoints | \ + jq -r '.data.endpoints[] | "\(.provider_name) | tag=\(.tag) | quant=\(.quantization) | ctx=\(.context_length) | prompt=\(.pricing.prompt) | comp=\(.pricing.completion) | cache=\(.pricing.input_cache_read) | uptime1d=\(.uptime_last_1d)"' + +# 2. Live-test candidate providers inside openclaw-0 with real nested tool_call payloads: +kubectl exec openclaw-0 -n openclaw -c openclaw -- python3 -c ' +import os, json, time, urllib.request +api_key = os.environ["OPENROUTER_API_KEY"] +tools = [{"type": "function", "function": {"name": "tool_call", "parameters": {"type": "object", "properties": {"id": {"type": "string"}, "args": {"type": "object"}}, "required": ["id", "args"]}}}] +candidates = ["deepinfra", "streamlake", "inference-net", "venice", "digitalocean", "novita", "parasail", "cloudflare", "nebius"] +for prov in candidates: + req = urllib.request.Request( + "https://openrouter.ai/api/v1/chat/completions", + data=json.dumps({ + "model": "deepseek/deepseek-v4.1-flash-1017", + "messages": [{"role": "user", "content": "Call tool_call with id=\"mcp:homeassistant:homeassistant__ha_get_state\" and args={\"entity_id\": \"media_player.bad\"}"}], + "tools": tools, "tool_choice": "auto", "max_tokens": 250, + "provider": {"order": [prov], "allow_fallbacks": False} + }).encode(), + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + ) + t0 = time.time() + try: + with urllib.request.urlopen(req, timeout=20) as r: + b = json.loads(r.read().decode()) + tc = b["choices"][0]["message"].get("tool_calls") + args = tc[0]["function"]["arguments"] if tc else "NONE" + print(f"PASS | {prov:15s} | {time.time()-t0:4.2f}s | actual={b.get(\"provider\")} | args={args}") + except urllib.error.HTTPError as e: + print(f"FAIL | {prov:15s} | HTTP {e.code} | {e.read().decode()[:140]}") + except Exception as e: + print(f"FAIL | {prov:15s} | ERR {e}") +' +``` + +--- + ## Consequences ### Positive -* **Unified Latest-Generation Stack**: Completely eliminates legacy Gemini 2.5 models; standardizes exclusively on Gemini 3.1, GLM 5.3, Whisper V3 Turbo, and Sonar Pro. +* **Unified Latest-Generation Stack**: Completely eliminates legacy Gemini 2.5 models; standardizes exclusively on DeepSeek V4.1 Flash 1017, Gemini 3.8/3.1, GLM 5.3, Whisper V3 Turbo, and Sonar Pro. * **100% Reliable Image Generation**: Correctly routes image generation requests to Nano Banana 2 (`google/gemini-3.1-flash-image`), completely eliminating HTTP 404 generation errors. * **Cohesive Voice & Audio Pipeline**: Clear architectural documentation of the Whisper V3 Turbo and Gemini 3.1 Flash TTS integration for dossiers and podcasts. -* **Guaranteed Cost Ceiling**: Drops weekly expenditure from **\$12.55 to ~\$1.20–\$1.80** (85–90% savings) through decoupled heartbeats and nightly compaction. +* **Guaranteed Cost Ceiling**: Drops weekly expenditure from **\$12.55 to ~\$0.80–\$1.40** (~90% savings) through DeepSeek V4.1 Flash 1017's `$0.00294/M` cache reads, decoupled heartbeats, and nightly compaction. ### Negative & Mitigations -* **OpenRouter Provider Variability**: GLM-5.3-Flash prefill throughput varies across OpenRouter hosting providers. - * *Mitigation*: Daily compaction bounds transcript size; failover to DeepSeek V4.1-Flash is available at zero architectural switching cost. +* **OpenRouter Provider Rollout Lag on Day-0 Model Releases**: Newly released snapshots initially launch with 5–10 providers before the full fleet (25+ providers) joins over subsequent days. + * *Mitigation*: Codified the 7-point provider qualification gate and one-shot test script above, and configured automatic model fallback to the `0910` snapshot (`openrouter/deepseek/deepseek-v4.1-flash`). diff --git a/k8s/openclaw/TROUBLESHOOTING.md b/k8s/openclaw/TROUBLESHOOTING.md index d168207..56b072a 100644 --- a/k8s/openclaw/TROUBLESHOOTING.md +++ b/k8s/openclaw/TROUBLESHOOTING.md @@ -966,4 +966,49 @@ This document captures fixed issues, operational quirks, and incident root-cause - **How to Avoid Recurrence**: - Always decouple perception tasks (OCR, image description, speech-to-text) into `tools.media` or `imageModel`, keeping primary conversational models focused on reasoning and dialogue without changing the core chat persona. +--- +### Issue 30: GLM-5.3-Flash Malformed Tool Call Stream Aborts (`streamlake/fp8`), Cold HDD Prompt Assembly Stall, and Fleet-Wide Migration to DeepSeek V4.1 Flash + +- **Date**: 2026-10-17 +- **Severity**: High (Topic 164 `#Grabengasse & Media` failed with `Provider returned an incomplete or malformed tool call`; 12 total occurrences across 5 agents in 17 days; 504.5s cold-disk event-loop stall on first morning request) +- **Symptom**: + 1. At 07:11:02 CEST, Moritz sent a voice note in Topic 164 (`#Grabengasse & Media`, agent `grabengasse_media`): *"Kannst du mir auf dem Sonos in der Küche die Bibi Blocksberg Playlist Der Affe ist los abspielen?"* + 2. The turn stalled for **8 minutes and 24 seconds** (`504,493 ms`) during `system-prompt` assembly (`cpuCoreRatio=0.005`, pure disk I/O wait) before sending the first LLM request at 07:19:26 CEST. + 3. In Turn 1 (`SEQ 175`), `z-ai/glm-5.3-flash` called `tool_search({"query": "sonos spotify play"})`. In Turn 2 (`SEQ 177`), it called `tool_describe({"name": "mcp:spotify:spotify__search"})`. + 4. In Turn 3 (`SEQ 179`, `responseId: gen-1792214444-T46fmmBLqgmCCe0YJp4X`), after generating 326 reasoning tokens (`"Search for 'Bibi Blocksberg Der Affe ist los' using mcp:spotify:spotify__search"`), the upstream OpenRouter provider (`streamlake/fp8`) aborted the stream mid-JSON while serializing nested `tool_call({"id": "mcp:spotify:spotify__search", "args": {...}})` arguments: + `stopReason: "error"`, `errorMessage: "Provider returned an incomplete or malformed tool call"`. + 5. OpenClaw only retries or triggers model fallbacks on HTTP status errors (`429`, `502`, `503`, `504`) prior to stream start—not when an HTTP `200 OK` SSE stream terminates mid-generation with a malformed tool-call chunk. +- **Root Cause & Fleet Forensics**: + 1. **`z-ai/glm-5.3-flash` Nested Tool-Call Fragility on `streamlake/fp8` & `siliconflow/fp8`**: + - Across October 1–17, 2026, `Provider returned an incomplete or malformed tool call` occurred **12 times** across `main` (6×), `grabengasse_media` (3×), `koch` (1×), `hebamme` (1×), and `doktor` (1×)—exclusively on `z-ai/glm-5.3-flash`, always right after `tool_describe` when emitting nested `tool_call` JSON payloads. + 2. **Suboptimal OpenRouter Provider Routing**: + - `extra_body.provider.order` for `z-ai/glm-5.3-flash` listed `"cloudflare"` first (which does not host `glm-5.3-flash` on OpenRouter), falling through to `streamlake/fp8` (91.7% uptime) and `siliconflow/fp8` (74.1% uptime), and did not block `fp4` (`nvfp4`) quantizations (`phala`, `atlas-cloud`, `parasail`). + 3. **Fleet-Wide Cost & Cache-Hit Economics (`DeepSeek V4.1 Flash` vs. `GLM 5.3 Flash`)**: + - Over the 17-day period (Oct 1–17, 2026), 73% to 94% of all tokens across interactive agents (`21.93M` `cacheRead` vs. `8.94M` uncached input) and background heartbeats (`22.86M` `cacheRead` vs. `7.73M` uncached input) were prompt cache reads. + - While `deepseek/deepseek-v4.1-flash` has a slightly higher base uncached prompt rate (`$0.14–$0.15/M` vs. `$0.06–$0.075/M`), its top `fp8` providers (`deepinfra` and `streamlake`, both 100% uptime and full `tool_choice` support) charge **3.5× to 10× less for `cacheRead` tokens** (`$0.0042/M` on DeepInfra, `$0.0030/M` on StreamLake vs. `$0.015–$0.030/M` on GLM 5.3 Flash). + - Consequently, running the entire fleet (interactive agents + background heartbeats) on `deepseek/deepseek-v4.1-flash` via `deepinfra`/`streamlake` costs **~$2.69 over 17 days (~$4.75/month)** vs. **$4.50 actual on `GLM 5.3 Flash` (~$7.95/month)**—a **~40% net cost reduction** alongside 0 malformed tool calls. + 4. **Cold Rotational HDD Read Stall After Nightly Backups**: + - Nightly backups (`03:30`) and compaction (`04:45`) evicted the Knowledge Wiki and agent workspace `*.md` files from the Linux kernel page cache, causing the first morning turn's synchronous `system-prompt` loader to block on rotational SATA HDD seeks for 504.5s. +- **Resolution**: + 1. **Fleet-Wide Migration to `openrouter/deepseek/deepseek-v4.1-flash-1017` (with `deepseek-v4.1-flash` 0910 as first fallback)**: + - Switched `agents.defaults.model.primary`, `utilityModel`, `heartbeat.model`, `subagents.model`, `memory-core.config.dreaming.model`, and all interactive agent entries (`main`, `fitness`, `koch`, `hebamme`, `travel`, `shopping`, `regensburg`, `recherche`, `doktor`, `itguy`, `grabengasse_media`, `mila_tracker`) to the newly released October 17, 2026 snapshot `openrouter/deepseek/deepseek-v4.1-flash-1017` (Artificial Analysis Intelligence Index 39.5 vs. 24.7 on 0910, and 30% cheaper on DeepInfra at `$0.098/M` prompt, `$0.294/M` completion, `$0.00294/M` `cacheRead`), with `openrouter/deepseek/deepseek-v4.1-flash`, `openrouter/z-ai/glm-5.3-flash`, and `openrouter/qwen/qwen3.8-flash` as fallbacks (`nutrition_tracker` remains on `openrouter/google/gemini-3.8-flash`). + 2. **Preserved Decoupled Vision Perception (Issue 29 Compatibility)**: + - Registered both `deepseek/deepseek-v4.1-flash-1017` and `deepseek/deepseek-v4.1-flash` with `input: ["text"]` under `models.providers.openrouter.models` in `openclaw-instance.secret.yaml` so OpenClaw continues routing all image attachments through `tools.media.image` (`google/gemini-3.8-flash`) for OCR before passing enriched text to DeepSeek V4.1 Flash. + 3. **Hardened OpenRouter Provider & Quantization Routing (`extra_body.provider`)**: + - Configured `openrouter/deepseek/deepseek-v4.1-flash-1017` with `order: ["deepinfra", "inference-net", "parasail", "cloudflare"]`, `quantizations: ["fp8", "bf16", "unknown"]`, and `ignore: ["morph", "baseten", "deepseek", "open-inference", "decart", "sail-research", "wafer"]` (live routing tests showed `morph/fp8` is rejected by OpenRouter's `Filter by Tool Compatibility` when `tools` are passed, `deepseek` is blocked by account training-opt-out guardrails, and `baseten` returns HTTP 429). + - Configured fallback `openrouter/deepseek/deepseek-v4.1-flash` (`0910`) with `order: ["deepinfra", "streamlake", "inference-net", "digitalocean", "venice", "nebius"]` and `quantizations: ["fp8", "bf16", "unknown"]`. + - Cleaned up `openrouter/z-ai/glm-5.3-flash` fallback routing (`order: ["morph", "near-ai", "digitalocean", "venice", "streamlake"]`, `quantizations: ["fp8", "bf16", "unknown"]`, blocking `fp4`). + 4. **Direct MCP Tool Fastpaths & 1-Turn Sonos Playbook for `grabengasse_media` (Topic 164)**: + - Added exact MCP tool IDs (`mcp:spotify:spotify__search`, `mcp:spotify:spotify__play`, `mcp:spotify:spotify__transfer_playback`, `mcp:jelly:jelly__jelly_search`, `mcp:jelly:jelly__jelly_request`, `mcp:homeassistant:homeassistant__ha_get_state`, `mcp:homeassistant:homeassistant__ha_call_service`) as well as all verified Sonos entity IDs (`media_player.bad`, `media_player.wohnzimmer`, `media_player.kuche_sonos`, `media_player.kuche_ikea`, `media_player.dining_room`) and Sonos `source_list` favorites (`"Fusion 2026 Sets"`, `"ChilloutTechno"`, `"deephouse"`, `"OberDITSCHI"`, etc.) directly to `openclaw-instance.secret.yaml`, `knowledge-wiki/agents/grabengasse_media.md`, and `/home/openclaw/.openclaw/workspace-grabengasse_media/AGENTS.md`. + - Eliminates both the 2 MCP discovery turns (`tool_search` + `tool_describe`) and the 2 file-read turns (`read("USER.md")`, `read("MEMORY.md")`, `read("SKILL.md")`). + 5. **Extended Linux Page-Cache Pre-Warming to Knowledge Wiki & Workspace Markdown Files**: + - Updated `k8s/openclaw/scripts/gateway-preflight.mjs` and `k8s/openclaw/scripts/compact_sessions.py` (which now also runs `prewarm_page_cache()` at the end of the 04:45 nightly run) to pre-read all Knowledge Wiki (`*.md`, `*.jsonl`) and `workspace*/` `*.md` files into RAM, preventing morning `system-prompt` disk freezes. + 6. **SQLite `session_nodes` Trigger (`entry_valid`) & Subagent Vector Re-Indexing Fix (`extraPaths: []`)**: + - Discovered that SQLite `session_nodes` has an `AFTER UPDATE OF entry_json` trigger (`session_nodes_entry_valid_after_entry_update`) that resets `entry_valid = 0` whenever `entry_json` is modified, throwing `SessionCanonicalKeyMigrationRequiredError` until `UPDATE session_nodes SET entry_valid = 1` is executed. + - Hardened `k8s/openclaw/scripts/recover_sessions.py` to always enforce `PRAGMA foreign_keys = ON` before deleting trailing error turns, set `entry_valid = 1` across all rows, and reset `attempts = 0` on pending `channel_ingress_events` in `state/openclaw.sqlite`. + - Explicitly set `memory.search.extraPaths: []` at the top level in `openclaw-instance.secret.yaml` and `dedup_subagent_vectors.py` so `mergeMode: merge` does not leave `extraPaths` active globally in `openclaw.json`, preventing subagents from re-embedding 4,059 wiki chunks on their first turn after startup. +- **How to Avoid Recurrence**: + - Always enforce `quantizations: ["fp8", "bf16"]` in OpenRouter `extra_body.provider` for tool-calling agents so OpenRouter never routes to degraded `fp4` (`nvfp4`) endpoints. + - Whenever updating `entry_json` in `session_nodes` via SQLite, always follow up with `UPDATE session_nodes SET entry_valid = 1` in a separate statement (after the `AFTER UPDATE OF entry_json` trigger fires), and always set `PRAGMA foreign_keys = ON` before deleting from `transcript_events`. + - Keep Knowledge Wiki and workspace Markdown files in the nightly `prewarm_page_cache()` pass so morning turns hit RAM (`<50ms`) instead of cold rotational disks. diff --git a/k8s/openclaw/configmap-scripts.yaml b/k8s/openclaw/configmap-scripts.yaml index 733226a..571dc9a 100644 --- a/k8s/openclaw/configmap-scripts.yaml +++ b/k8s/openclaw/configmap-scripts.yaml @@ -1,907 +1,633 @@ apiVersion: v1 +data: + 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.\nTIER_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 and Knowledge + Wiki 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 \"find /home/node/.openclaw/wiki/main + /home/node/.openclaw/workspace* -maxdepth 3 -name '*.md' -exec cat {} + > /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\" ✓ Truncated successfully: + {r.stdout.strip()}\")\n else:\n print(f\" ✗ + 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\" ✗ [{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\" ✓ Deep-compacted: {r.stdout.strip()}\")\n + \ elif \"Already compacted\" in r.stderr or \"Already compacted\" in + r.stdout:\n print(f\" ✓ Already compacted.\")\n else:\n + \ print(f\" ✗ 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\" ✓ Truncation + fallback success: {fb.stdout.strip()}\")\n else:\n print(f\" + \ ✗ Truncation fallback failed: {fb.stderr.strip()}\")\n except Exception + as e:\n print(f\" ✗ 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\" + \ ✓ {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\" ✓ Already compacted.\")\n skipped + += 1\n else:\n print(f\" ✗ {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\" ✓ (fallback) {fb_res.stdout.strip()}\")\n + \ compacted += 1\n else:\n print(f\" + \ ✗ 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\" ✗ 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\" ✗ 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 prewarm_page_cache()\n\nif + __name__ == \"__main__\":\n main()\n" + dedup_subagent_vectors.py: | + #!/usr/bin/env python3 + import glob, json, os, sqlite3, sys + + cfg_path = "/home/node/.openclaw/openclaw.json" + if os.path.exists(cfg_path): + try: + with open(cfg_path, "r") as f: + cfg = json.load(f) + mem_search = cfg.get("memory", {}).get("search", {}) + if mem_search.get("extraPaths"): + mem_search["extraPaths"] = [] + with open(cfg_path, "w") as f: + json.dump(cfg, f, indent=2) + print("[dedup_subagents] Cleared top-level memory.search.extraPaths from openclaw.json (scoped to main only)") + except Exception as e: + print(f"[dedup_subagents] Warning checking openclaw.json: {e}", file=sys.stderr) + + 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) + gateway-preflight.mjs: | + 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; find /home/node/.openclaw/wiki/main /home/node/.openclaw/workspace* -maxdepth 3 -name '*.md' -exec cat {} + > /dev/null 2>&1 || true"); + console.log(`[gateway-preflight] Pre-warmed SQLite databases and Wiki markdown 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.`); + } + } + openclaw-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\nimport + 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(\"⚠️ + [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(\"⚡ [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\" ⚙️ 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\" + \ ❌ [SEQ {seq}] TOOL ERROR ({tname}): {res_txt[:260]}\")\n else:\n + \ print(f\" ✅ [SEQ {seq}] TOOL RESULT ({tname}): {res_txt[:180]}\")\n\n + \ if last_role == \"user\":\n print(\"\\n⚠️ 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" + recover_sessions.py: |+ + #!/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, status FROM session_nodes WHERE status IN (\"running\", \"failed\", \"error\")") + rows = cursor.fetchall() + agent_id = os.path.basename(os.path.dirname(os.path.dirname(db))) + for sk, ej, csid, st 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 st == "running" and 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)) + elif msg.get("role") == "assistant" and msg.get("stopReason") == "error": + 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_nodes SET entry_valid = 1 WHERE entry_valid != 1") + cursor.execute("UPDATE session_windows SET status = \"done\" WHERE status IN (\"running\", \"failed\", \"error\")") + conn.commit() + conn.close() + except Exception as e: + print(f"[recover_sessions] Warning for {db}: {e}", file=sys.stderr) + + state_db = "/home/node/.openclaw/state/openclaw.sqlite" + if os.path.exists(state_db): + try: + sconn = sqlite3.connect(state_db, timeout=10.0) + scur = sconn.cursor() + scur.execute("UPDATE channel_ingress_events SET attempts = 0, last_attempt_at = NULL, last_error = NULL WHERE status = 'pending' AND attempts > 0") + sconn.commit() + sconn.close() + except Exception as e: + print(f"[recover_sessions] Warning resetting ingress backoff: {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)") + + replay_interrupted_turns.py: | + #!/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 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" - 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" - dedup_subagent_vectors.py: "#!/usr/bin/env python3\nimport glob, os, sqlite3, sys\n\ - \nvec_path = \"/app/node_modules/sqlite-vec-linux-x64/vec0.so\"\nfor 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" - 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" - gateway-preflight.mjs: "import fs from \"node:fs\";\nimport path from \"node:path\"\ - ;\nimport { execSync, spawn } from \"node:child_process\";\n\nif (!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" - openclaw-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" - 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" - recover_sessions.py: "#!/usr/bin/env python3\nimport glob, sqlite3, json, os, sys,\ - \ time\n\nt0 = time.time()\nrecovered = 0\ninterrupted_turns = []\n\ndef 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\nfor 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\nif 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\nprint(f\"[recover_sessions] Completed\ - \ in {time.time() - t0:.2f}s ({recovered} wedged sessions recovered)\")\n" - replay_interrupted_turns.py: "#!/usr/bin/env python3\nimport json, os, subprocess,\ - \ sys, time, urllib.request\n\nINTERRUPTED_FILE = \"/tmp/openclaw-interrupted-turns.json\"\ - \n\nif not os.path.exists(INTERRUPTED_FILE):\n sys.exit(0)\n\ntry:\n with\ - \ open(INTERRUPTED_FILE, \"r\") as f:\n turns = json.load(f)\nexcept Exception\ - \ as e:\n print(f\"[replay_interrupted_turns] Failed to read {INTERRUPTED_FILE}:\ - \ {e}\", file=sys.stderr)\n sys.exit(0)\n\nif not turns:\n sys.exit(0)\n\ - \nprint(f\"[replay_interrupted_turns] Found {len(turns)} interrupted turn(s).\ - \ Waiting for gateway /healthz...\")\n\ngateway_ready = False\nfor _ 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\ - \nif 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\ntime.sleep(5)\n\nfor 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\ntry:\n os.remove(INTERRUPTED_FILE)\n\ - \ print(f\"[replay_interrupted_turns] Cleaned up {INTERRUPTED_FILE}\")\nexcept\ - \ Exception:\n pass\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\n\ntry:\n\ - \ import yaml\nexcept ImportError:\n venv_py = \"/home/node/.openclaw/mcp-venv/bin/python3\"\ - \n if os.path.exists(venv_py) and sys.executable != venv_py:\n os.execv(venv_py,\ - \ [venv_py] + sys.argv)\n raise\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" - 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/crons.yaml b/k8s/openclaw/crons.yaml index 497d02d..3088045 100644 --- a/k8s/openclaw/crons.yaml +++ b/k8s/openclaw/crons.yaml @@ -34,7 +34,7 @@ jobs: expr: "30 3 * * *" tz: "Europe/Berlin" agent: "fitness" - model: "openrouter/deepseek/deepseek-v4.1-flash" + model: "openrouter/deepseek/deepseek-v4.1-flash-1017" session: "isolated" channel: "telegram" to: "-1004326921510" @@ -58,7 +58,7 @@ jobs: expr: "0 3 * * 0" tz: "Europe/Berlin" agent: "itguy" - model: "openrouter/deepseek/deepseek-v4.1-flash" + model: "openrouter/deepseek/deepseek-v4.1-flash-1017" session: "isolated" channel: "telegram" to: "-1004326921510" @@ -83,7 +83,7 @@ jobs: expr: "30 6 * * 5" tz: "Europe/Berlin" agent: "recherche" - model: "openrouter/deepseek/deepseek-v4.1-flash" + model: "openrouter/deepseek/deepseek-v4.1-flash-1017" session: "isolated" channel: "telegram" to: "-1004326921510" diff --git a/k8s/openclaw/openclaw-instance.secret.yaml b/k8s/openclaw/openclaw-instance.secret.yaml index 2a64040e7394435db86a6ec558f658024120ff90..67ee73d1ff5e86ccd0d08e0ec9184cd97146a6cd 100644 GIT binary patch literal 62837 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

f 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

?vKS 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 /dev/null 2>&1 || true", + "cat /home/node/.openclaw/state/openclaw.sqlite /home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite > /dev/null 2>&1 || true; " + "find /home/node/.openclaw/wiki/main /home/node/.openclaw/workspace* -maxdepth 3 -name '*.md' -exec cat {} + > /dev/null 2>&1 || true", shell=True, timeout=90.0 ) @@ -329,6 +330,7 @@ def main(): compact_active_sessions() checkpoint_wal_journals() vacuum_and_optimize() + prewarm_page_cache() if __name__ == "__main__": main() diff --git a/k8s/openclaw/scripts/dedup_subagent_vectors.py b/k8s/openclaw/scripts/dedup_subagent_vectors.py index b4752ac..5434578 100644 --- a/k8s/openclaw/scripts/dedup_subagent_vectors.py +++ b/k8s/openclaw/scripts/dedup_subagent_vectors.py @@ -1,34 +1,48 @@ - #!/usr/bin/env python3 - import glob, os, sqlite3, sys +#!/usr/bin/env python3 +import glob, json, 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) +cfg_path = "/home/node/.openclaw/openclaw.json" +if os.path.exists(cfg_path): + try: + with open(cfg_path, "r") as f: + cfg = json.load(f) + mem_search = cfg.get("memory", {}).get("search", {}) + if mem_search.get("extraPaths"): + mem_search["extraPaths"] = [] + with open(cfg_path, "w") as f: + json.dump(cfg, f, indent=2) + print("[dedup_subagents] Cleared top-level memory.search.extraPaths from openclaw.json (scoped to main only)") + except Exception as e: + print(f"[dedup_subagents] Warning checking openclaw.json: {e}", file=sys.stderr) + +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/gateway-preflight.mjs b/k8s/openclaw/scripts/gateway-preflight.mjs index 0c6b2f9..a536d45 100644 --- a/k8s/openclaw/scripts/gateway-preflight.mjs +++ b/k8s/openclaw/scripts/gateway-preflight.mjs @@ -1,78 +1,78 @@ - import fs from "node:fs"; - import path from "node:path"; - import { execSync, spawn } from "node:child_process"; +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"; +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"); + 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..."); + 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.`); + // 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; find /home/node/.openclaw/wiki/main /home/node/.openclaw/workspace* -maxdepth 3 -name '*.md' -exec cat {} + > /dev/null 2>&1 || true"); + console.log(`[gateway-preflight] Pre-warmed SQLite databases and Wiki markdown 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/openclaw-triage.py b/k8s/openclaw/scripts/openclaw-triage.py index 3c993d2..188c9b9 100644 --- a/k8s/openclaw/scripts/openclaw-triage.py +++ b/k8s/openclaw/scripts/openclaw-triage.py @@ -132,7 +132,7 @@ def triage_agent_session(agent, topic_id=None, limit=10, db_root="/home/node/.op events.reverse() print("\n📜 RECENT TRANSCRIPT EVENTS (Chronological):") - + last_role = None for seq, ca_ms, ej_str in events: try: diff --git a/k8s/openclaw/scripts/recover_sessions.py b/k8s/openclaw/scripts/recover_sessions.py index 8db7cc6..c8840c5 100644 --- a/k8s/openclaw/scripts/recover_sessions.py +++ b/k8s/openclaw/scripts/recover_sessions.py @@ -1,80 +1,96 @@ - #!/usr/bin/env python3 - import glob, sqlite3, json, os, sys, time +#!/usr/bin/env python3 +import glob, sqlite3, json, os, sys, time - t0 = time.time() - recovered = 0 - interrupted_turns = [] +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 "") +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) +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, status FROM session_nodes WHERE status IN (\"running\", \"failed\", \"error\")") + rows = cursor.fetchall() + agent_id = os.path.basename(os.path.dirname(os.path.dirname(db))) + for sk, ej, csid, st 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 st == "running" and 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)) + elif msg.get("role") == "assistant" and msg.get("stopReason") == "error": + 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_nodes SET entry_valid = 1 WHERE entry_valid != 1") + cursor.execute("UPDATE session_windows SET status = \"done\" WHERE status IN (\"running\", \"failed\", \"error\")") + 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) +state_db = "/home/node/.openclaw/state/openclaw.sqlite" +if os.path.exists(state_db): + try: + sconn = sqlite3.connect(state_db, timeout=10.0) + scur = sconn.cursor() + scur.execute("UPDATE channel_ingress_events SET attempts = 0, last_attempt_at = NULL, last_error = NULL WHERE status = 'pending' AND attempts > 0") + sconn.commit() + sconn.close() + except Exception as e: + print(f"[recover_sessions] Warning resetting ingress backoff: {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)") - 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 index 47f5e0a..6774e1b 100644 --- a/k8s/openclaw/scripts/replay_interrupted_turns.py +++ b/k8s/openclaw/scripts/replay_interrupted_turns.py @@ -1,97 +1,97 @@ - #!/usr/bin/env python3 - import json, os, subprocess, sys, time, urllib.request +#!/usr/bin/env python3 +import json, os, subprocess, sys, time, urllib.request - INTERRUPTED_FILE = "/tmp/openclaw-interrupted-turns.json" +INTERRUPTED_FILE = "/tmp/openclaw-interrupted-turns.json" - if not os.path.exists(INTERRUPTED_FILE): - sys.exit(0) +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) +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) +if not turns: + sys.exit(0) - print(f"[replay_interrupted_turns] Found {len(turns)} interrupted turn(s). Waiting for gateway /healthz...") +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) +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) +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) +# 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 +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 "" - 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) + channel = ctx.get("channel") or "telegram" + to_target = ctx.get("to") or "" + thread_id = ctx.get("threadId") - # 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) + target = to_target + if target.startswith("telegram:"): + parts = target.split(":") + target = parts[1] if len(parts) > 1 else target - try: - os.remove(INTERRUPTED_FILE) - print(f"[replay_interrupted_turns] Cleaned up {INTERRUPTED_FILE}") - except Exception: - pass + 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/setup-garmin-cron.sh b/k8s/openclaw/setup-garmin-cron.sh index 9ee2644..cc984b2 100755 --- a/k8s/openclaw/setup-garmin-cron.sh +++ b/k8s/openclaw/setup-garmin-cron.sh @@ -54,7 +54,7 @@ kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron a --cron "$CRON_SCHEDULE" \ --tz "$TIMEZONE" \ --agent "fitness" \ - --model "openrouter/deepseek/deepseek-v4.1-flash" \ + --model "openrouter/deepseek/deepseek-v4.1-flash-1017" \ --session "isolated" \ --channel "telegram" \ --to "$TELEGRAM_GROUP" \ diff --git a/k8s/openclaw/setup-itguy-cron.sh b/k8s/openclaw/setup-itguy-cron.sh index fa7e272..f32c313 100755 --- a/k8s/openclaw/setup-itguy-cron.sh +++ b/k8s/openclaw/setup-itguy-cron.sh @@ -48,7 +48,7 @@ kubectl exec -n "$NAMESPACE" "$POD_NAME" -c "$CONTAINER_NAME" -- openclaw cron a --cron "$CRON_SCHEDULE" \ --tz "$TIMEZONE" \ --agent "itguy" \ - --model "openrouter/deepseek/deepseek-v4.1-flash" \ + --model "openrouter/deepseek/deepseek-v4.1-flash-1017" \ --session "isolated" \ --channel "telegram" \ --to "$TELEGRAM_GROUP" \ diff --git a/knowledge-wiki b/knowledge-wiki index 419b006..720c5cf 160000 --- a/knowledge-wiki +++ b/knowledge-wiki @@ -1 +1 @@ -Subproject commit 419b00657436708969a5f69fe00baee998fe9c48 +Subproject commit 720c5cf74bf9625c88411649ff9ca2724c18119a diff --git a/projects/duckdb-backend/app/engine.py b/projects/duckdb-backend/app/engine.py index 1e26478..a753fb4 100644 --- a/projects/duckdb-backend/app/engine.py +++ b/projects/duckdb-backend/app/engine.py @@ -734,44 +734,93 @@ class DuckDBEngine: today_wet_count = int(diaper_agg[1]) if diaper_agg and diaper_agg[1] is not None else 0 today_dirty_count = int(diaper_agg[2]) if diaper_agg and diaper_agg[2] is not None else 0 - # 5. 7d and 30d sleep averages + # 5. 7d and 30d sleep averages (use completed days < today_str when available) avg_7 = self.con.execute( - """ + f""" SELECT ROUND(AVG(total_sleep_hours), 2), ROUND(AVG(night_sleep_minutes)/60.0, 2), ROUND(AVG(nap_sleep_minutes)/60.0, 2), ROUND(AVG(nap_count), 1) - FROM (SELECT total_sleep_hours, night_sleep_minutes, nap_sleep_minutes, nap_count - FROM v_sleep_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7) + FROM ( + SELECT total_sleep_hours, night_sleep_minutes, nap_sleep_minutes, nap_count + FROM v_sleep_daily + WHERE subject = 'mila' AND log_date < '{today_str}' + ORDER BY log_date DESC LIMIT 7 + ) """ ).fetchone() + if not avg_7 or avg_7[0] is None: + avg_7 = self.con.execute( + """ + SELECT ROUND(AVG(total_sleep_hours), 2), ROUND(AVG(night_sleep_minutes)/60.0, 2), + ROUND(AVG(nap_sleep_minutes)/60.0, 2), ROUND(AVG(nap_count), 1) + FROM (SELECT total_sleep_hours, night_sleep_minutes, nap_sleep_minutes, nap_count + FROM v_sleep_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7) + """ + ).fetchone() avg_7_tot = f"{avg_7[0]:.2f} h" if avg_7 and avg_7[0] is not None else "—" avg_7_night = f"{avg_7[1]:.2f} h" if avg_7 and avg_7[1] is not None else "—" avg_7_naps = f"{avg_7[3]} Naps ({avg_7[2]:.2f} h)" if avg_7 and avg_7[3] is not None else "—" avg_30 = self.con.execute( - """ + f""" SELECT ROUND(AVG(total_sleep_hours), 2) - FROM (SELECT total_sleep_hours FROM v_sleep_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 30) + FROM ( + SELECT total_sleep_hours FROM v_sleep_daily + WHERE subject = 'mila' AND log_date < '{today_str}' + ORDER BY log_date DESC LIMIT 30 + ) """ ).fetchone() + if not avg_30 or avg_30[0] is None: + avg_30 = self.con.execute( + """ + SELECT ROUND(AVG(total_sleep_hours), 2) + FROM (SELECT total_sleep_hours FROM v_sleep_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 30) + """ + ).fetchone() avg_30_tot = f"{avg_30[0]:.2f} h" if avg_30 and avg_30[0] is not None else "—" feed_avg_7 = self.con.execute( - """ + f""" SELECT ROUND(AVG(total_consumed_ml), 0), ROUND(AVG(total_solid_g), 0), ROUND(AVG(total_kcal), 0), ROUND(AVG(bottle_count), 1) - FROM (SELECT total_consumed_ml, total_solid_g, total_kcal, bottle_count - FROM v_feeding_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7) + FROM ( + SELECT total_consumed_ml, total_solid_g, total_kcal, bottle_count + FROM v_feeding_daily + WHERE subject = 'mila' AND log_date < '{today_str}' + ORDER BY log_date DESC LIMIT 7 + ) """ ).fetchone() + if not feed_avg_7 or feed_avg_7[0] is None: + feed_avg_7 = self.con.execute( + """ + SELECT ROUND(AVG(total_consumed_ml), 0), ROUND(AVG(total_solid_g), 0), + ROUND(AVG(total_kcal), 0), ROUND(AVG(bottle_count), 1) + FROM (SELECT total_consumed_ml, total_solid_g, total_kcal, bottle_count + FROM v_feeding_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7) + """ + ).fetchone() diaper_avg_7 = self.con.execute( - """ + f""" SELECT ROUND(AVG(wet_count), 1), ROUND(AVG(total_changes), 1) - FROM (SELECT wet_count, total_changes - FROM v_diaper_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7) + FROM ( + SELECT wet_count, total_changes + FROM v_diaper_daily + WHERE subject = 'mila' AND log_date < '{today_str}' + ORDER BY log_date DESC LIMIT 7 + ) """ ).fetchone() + if not diaper_avg_7 or diaper_avg_7[0] is None: + diaper_avg_7 = self.con.execute( + """ + SELECT ROUND(AVG(wet_count), 1), ROUND(AVG(total_changes), 1) + FROM (SELECT wet_count, total_changes + FROM v_diaper_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7) + """ + ).fetchone() # 6. Past 30 Days History Table & sessions history_rows_data = self.con.execute( @@ -837,10 +886,18 @@ class DuckDBEngine: for r in history_rows_data: d_str = str(r[0]) d_obj = datetime.date.fromisoformat(d_str) - nh, nm = divmod(r[2] or 0, 60) - d_sessions = sessions_by_date.get(d_str, []) + d_sessions = list(sessions_by_date.get(d_str, [])) + row_night_min = int(r[2] or 0) + row_nap_min = int(r[4] or 0) + row_tot_hours = round(r[1], 2) if r[1] is not None else 0.0 + + if d_str == today_str and row_night_min == 0 and today_night_min > 0: + row_night_min = today_night_min + row_tot_hours = round((row_night_min + row_nap_min) / 60.0, 2) + + nh, nm = divmod(row_night_min, 60) night_durs = [s["duration_minutes"] for s in d_sessions if s["type"] == "night" and s["duration_minutes"]] - longest_night = max(night_durs) if night_durs else int(r[2] or 0) + longest_night = max(night_durs) if night_durs else row_night_min wake_windows = [] day_sessions = [s for s in d_sessions if s["type"] == "nap" and s.get("local_start") and s.get("local_end")] @@ -860,12 +917,12 @@ class DuckDBEngine: "weekday": WEEKDAYS_DE[d_obj.weekday()], "formatted_date": d_obj.strftime("%d.%m."), "is_today": (d_str == today_str), - "total_hours": round(r[1], 2) if r[1] is not None else 0.0, - "night_minutes": int(r[2] or 0), + "total_hours": row_tot_hours, + "night_minutes": row_night_min, "night_duration_str": f"{nh} h {nm:02d} min" if nh > 0 else f"{nm} min", - "nap_minutes": int(r[4] or 0), + "nap_minutes": row_nap_min, "nap_count": int(r[3] or 0), - "nap_str": f"{r[3]} ({r[4]} min)", + "nap_str": f"{r[3]} ({row_nap_min} min)", "longest_night_stretch_min": longest_night, "avg_wake_window_min": avg_wake, "feed_bottles_ml": int(r[5]), @@ -1457,6 +1514,37 @@ class DuckDBEngine: for r in records: f.write(json.dumps(r, ensure_ascii=False) + "\n") + def _parse_event_dt( + self, + raw: str, + now_dt: datetime.datetime, + actor: str = "", + base_date: Optional[str] = None, + ) -> datetime.datetime: + s = raw.strip() + m_time = re.fullmatch(r"^(\d{1,2}):(\d{2})(?::(\d{2}))?$", s) + if m_time: + d_part = base_date or now_dt.astimezone(BERLIN_TZ).strftime("%Y-%m-%d") + hh = int(m_time.group(1)) + mm = int(m_time.group(2)) + ss = int(m_time.group(3) or 0) + return datetime.datetime.fromisoformat(f"{d_part}T{hh:02d}:{mm:02d}:{ss:02d}").replace(tzinfo=BERLIN_TZ) + + dt = datetime.datetime.fromisoformat(s.replace("Z", "+00:00")) + if dt.tzinfo is None: + return dt.replace(tzinfo=BERLIN_TZ) + + if s.endswith("+00:00") or s.endswith("Z"): + dt_wall_berlin = dt.replace(tzinfo=BERLIN_TZ) + # Guard against LLM agents formatting local Europe/Berlin wall-clock times with +00:00 / Z + if ( + (dt > now_dt + datetime.timedelta(minutes=5) and dt_wall_berlin <= now_dt + datetime.timedelta(minutes=5)) + or ((actor or "").startswith("agent:") and dt.second == 0 and dt.microsecond == 0) + ): + return dt_wall_berlin + + return dt.astimezone(BERLIN_TZ) + def add_event(self, req: EventCreateRequest) -> Dict[str, Any]: with self._lock: event_id = str(ULID()) @@ -1476,9 +1564,7 @@ class DuckDBEngine: start_dt = (now_dt - delta).astimezone(BERLIN_TZ) start_iso = start_dt.isoformat() else: - start_dt = datetime.datetime.fromisoformat(req.start) - if start_dt.tzinfo is None: - start_dt = start_dt.replace(tzinfo=BERLIN_TZ) + start_dt = self._parse_event_dt(req.start, now_dt, actor=req.actor, base_date=req.date) start_iso = start_dt.isoformat() elif req.since: delta = parse_duration(req.since) @@ -1488,21 +1574,48 @@ class DuckDBEngine: start_dt = now_dt.astimezone(BERLIN_TZ) start_iso = start_dt.isoformat() else: - start_dt = datetime.datetime.fromisoformat(req.start) - if start_dt.tzinfo is None: - start_dt = start_dt.replace(tzinfo=BERLIN_TZ) + start_dt = self._parse_event_dt(req.start, now_dt, actor=req.actor) start_iso = start_dt.isoformat() end_iso = None end_dt = None if req.end: - end_dt = datetime.datetime.fromisoformat(req.end) - if end_dt.tzinfo is None: - end_dt = end_dt.replace(tzinfo=BERLIN_TZ) + end_dt = self._parse_event_dt( + req.end, + now_dt, + actor=req.actor, + base_date=start_dt.astimezone(BERLIN_TZ).strftime("%Y-%m-%d"), + ) if end_dt < start_dt: raise ValueError(f"End time {req.end} cannot be earlier than start time {req.start}") end_iso = end_dt.isoformat() + # Populate Mila standard recipe defaults when omitted by tracking agents + if req.subject == "mila": + if req.kind == "feeding.solid" or (req.kind == "nutrition.intake" and "brei" in str(req.payload.get("item") or req.payload.get("type") or "").lower()): + if req.kind == "nutrition.intake": + req.kind = "feeding.solid" + has_grams = any(req.payload.get(k) not in (None, 0) for k in ("amount_g", "consumed_g", "qty")) + if not has_grams: + item_l = str(req.payload.get("item") or req.meal or "").lower() + if any(w in item_l for w in ("abend", "nachmittag", "dinner")): + req.payload["amount_g"] = 200 + req.payload.setdefault("kcal", 110) + else: + req.payload["amount_g"] = 190 + req.payload.setdefault("kcal", 135) + elif req.kind == "feeding.bottle": + c_ml = req.payload.get("consumed_ml") + a_ml = req.payload.get("amount_ml") + l_ml = req.payload.get("left_ml") + if (c_ml is None or c_ml == 0) and (a_ml is None or a_ml == 0): + if l_ml is not None: + req.payload["amount_ml"] = 270 + req.payload["consumed_ml"] = max(0, 270 - int(l_ml)) + else: + req.payload["amount_ml"] = 270 + req.payload["consumed_ml"] = 270 + # 15-Minute Duplicate Guard if not req.allow_duplicate: if req.kind in ("nutrition.intake", "feeding.bottle", "feeding.solid"): @@ -1722,9 +1835,7 @@ class DuckDBEngine: delta = parse_duration(req.since) end_dt = (now_dt - delta).astimezone(BERLIN_TZ) elif req.end_time: - end_dt = datetime.datetime.fromisoformat(req.end_time) - if end_dt.tzinfo is None: - end_dt = end_dt.replace(tzinfo=BERLIN_TZ) + end_dt = self._parse_event_dt(req.end_time, now_dt, actor=req.actor) else: end_dt = now_dt.astimezone(BERLIN_TZ) diff --git a/projects/duckdb-backend/app/git_sync.py b/projects/duckdb-backend/app/git_sync.py index ebf2eb1..93d89cd 100644 --- a/projects/duckdb-backend/app/git_sync.py +++ b/projects/duckdb-backend/app/git_sync.py @@ -43,7 +43,7 @@ class GitSyncManager: try: subprocess.run(["git", "config", "--global", "http.sslVerify", "false"], check=False) subprocess.run(["git", "config", "--global", "--add", "safe.directory", "*"], check=False) - if (self.wiki_dir / ".git").is_dir(): + if (self.wiki_dir / ".git").exists(): subprocess.run(["git", "-C", str(self.wiki_dir), "config", "user.name", GIT_AUTHOR_NAME], check=False) subprocess.run(["git", "-C", str(self.wiki_dir), "config", "user.email", GIT_AUTHOR_EMAIL], check=False) except Exception as e: @@ -64,7 +64,7 @@ class GitSyncManager: """Ensures repository exists and is initialized.""" with self._lock: self._configure_git_client() - if (self.wiki_dir / ".git").is_dir(): + if (self.wiki_dir / ".git").exists(): try: res = subprocess.run( ["git", "-C", str(self.wiki_dir), "rev-parse", "--short", "HEAD"], @@ -144,7 +144,7 @@ class GitSyncManager: def pull_rebase(self) -> bool: """Pulls latest changes via rebase. Handles conflicts cleanly.""" - if not (self.wiki_dir / ".git").is_dir(): + if not (self.wiki_dir / ".git").exists(): return False with self._lock: @@ -208,7 +208,7 @@ class GitSyncManager: def commit_and_push(self, commit_msg: str) -> bool: """Stages data/, commits with descriptive message, rebases and pushes.""" - if not (self.wiki_dir / ".git").is_dir(): + if not (self.wiki_dir / ".git").exists(): return False with self._lock: diff --git a/projects/duckdb-backend/app/main.py b/projects/duckdb-backend/app/main.py index 56fc9e0..1c4a990 100644 --- a/projects/duckdb-backend/app/main.py +++ b/projects/duckdb-backend/app/main.py @@ -21,23 +21,26 @@ logger = logging.getLogger("duckdb_backend") @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Initializing DuckDB Backend...") - git_sync = GitSyncManager( - wiki_dir=WIKI_DIR, - on_data_pulled=lambda: app.state.engine.reload_all() if hasattr(app.state, "engine") else None, - ) - git_sync.ensure_repo() - app.state.git_sync = git_sync + if getattr(app.state, "git_sync", None) is None or getattr(app.state, "engine", None) is None: + git_sync = GitSyncManager( + wiki_dir=WIKI_DIR, + on_data_pulled=lambda: app.state.engine.reload_all() if hasattr(app.state, "engine") else None, + ) + git_sync.ensure_repo() + app.state.git_sync = git_sync - engine = DuckDBEngine(data_dir=DATA_DIR, git_sync_manager=git_sync) - app.state.engine = engine + engine = DuckDBEngine(data_dir=DATA_DIR, git_sync_manager=git_sync) + app.state.engine = engine - await git_sync.start_periodic_sync() - logger.info("DuckDB Backend is ready to serve queries.") - - yield - - logger.info("Shutting down DuckDB Backend...") - await git_sync.stop_periodic_sync() + await git_sync.start_periodic_sync() + logger.info("DuckDB Backend is ready to serve queries.") + try: + yield + finally: + logger.info("Shutting down DuckDB Backend...") + await git_sync.stop_periodic_sync() + else: + yield app = FastAPI( diff --git a/projects/duckdb-backend/tests/conftest.py b/projects/duckdb-backend/tests/conftest.py index 5e76595..26cb453 100644 --- a/projects/duckdb-backend/tests/conftest.py +++ b/projects/duckdb-backend/tests/conftest.py @@ -13,13 +13,14 @@ from app.main import app @pytest.fixture(scope="session") def test_data_dir(): - # Use real wiki data dir if exists, else temporary copy + temp_root = Path(tempfile.mkdtemp()) + temp_data = temp_root / "data" if DATA_DIR.is_dir(): - yield DATA_DIR + shutil.copytree(DATA_DIR, temp_data) else: - temp_dir = Path(tempfile.mkdtemp()) - yield temp_dir - shutil.rmtree(temp_dir, ignore_errors=True) + temp_data.mkdir(parents=True, exist_ok=True) + yield temp_data + shutil.rmtree(temp_root, ignore_errors=True) @pytest.fixture diff --git a/projects/duckdb-backend/tests/test_api_events.py b/projects/duckdb-backend/tests/test_api_events.py index 1230215..e243b74 100644 --- a/projects/duckdb-backend/tests/test_api_events.py +++ b/projects/duckdb-backend/tests/test_api_events.py @@ -127,3 +127,50 @@ def test_duplicate_open_sleep_api_endpoint(client: TestClient): "subject": "test_baby", }) assert stop_res.status_code == 200 + + +def test_agent_utc_wallclock_guard_and_mila_recipe_defaults(tmp_path: Path): + engine = DuckDBEngine(data_dir=tmp_path) + + # 1. Agent passes +00:00 on local wall-clock times (e.g. 08:30 to 10:20) + nap_req = EventCreateRequest( + kind="sleep.session", + subject="mila", + actor="agent:mila_tracker", + start="2026-10-17T08:30:00+00:00", + end="2026-10-17T10:20:00+00:00", + payload={"type": "nap", "location": "bett"}, + ) + res_nap = engine.add_event(nap_req) + assert res_nap["status"] == "success" + assert res_nap["record"]["startIso"] == "2026-10-17T08:30:00+02:00" + assert res_nap["record"]["endIso"] == "2026-10-17T10:20:00+02:00" + assert res_nap["record"]["sleepTimeSeconds"] == 6600 + + # 2. Solid food without amount_g gets standard recipe defaults + brei_req = EventCreateRequest( + kind="feeding.solid", + subject="mila", + actor="agent:mila_tracker", + start="2026-10-17T12:20:00+00:00", + meal="lunch", + payload={"item": "mittagsbrei"}, + ) + res_brei = engine.add_event(brei_req) + assert res_brei["record"]["start"] == "2026-10-17T12:20:00+02:00" + assert res_brei["record"]["payload"]["amount_g"] == 190 + assert res_brei["record"]["payload"]["kcal"] == 135 + + abend_req = EventCreateRequest( + kind="feeding.solid", + subject="mila", + actor="agent:mila_tracker", + start="2026-10-17T16:40:00+00:00", + meal="dinner", + payload={"item": "abendbrei"}, + ) + res_abend = engine.add_event(abend_req) + assert res_abend["record"]["start"] == "2026-10-17T16:40:00+02:00" + assert res_abend["record"]["payload"]["amount_g"] == 200 + assert res_abend["record"]["payload"]["kcal"] == 110 + diff --git a/projects/mila/.dockerignore b/projects/mila/.dockerignore new file mode 100644 index 0000000..c6590c8 --- /dev/null +++ b/projects/mila/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +.git +.DS_Store diff --git a/projects/mila/src/App.vue b/projects/mila/src/App.vue index 64800c3..16dda6b 100644 --- a/projects/mila/src/App.vue +++ b/projects/mila/src/App.vue @@ -178,7 +178,16 @@ const liveCounterStr = computed(() => { // Wake Window status evaluation (sweet spot 135 to 180 min, i.e. 2h15m to 3h00m) const wakeWindowStatus = computed(() => { if (!data.value || data.value.live_status.is_sleeping) return null - const mins = data.value.live_status.wake_window_minutes ?? 0 + const ls = data.value.live_status + let mins = ls.wake_window_minutes ?? 0 + if (ls.last_wake_iso) { + try { + const wakeTime = new Date(ls.last_wake_iso).getTime() + if (!isNaN(wakeTime)) { + mins = Math.max(0, Math.floor((nowEpoch.value - wakeTime) / 60000)) + } + } catch {} + } if (mins < 135) { return { text: 'Entspannt wach', @@ -255,8 +264,21 @@ const lastFeedDetail = computed(() => { const diaperStatus = computed(() => { if (!data.value) return null const ls = data.value.live_status - const elapsed = ls.last_diaper_since_minutes ?? (ls as any).last_diaper_elapsed_minutes - const elapsedStool = (ls as any).last_stool_elapsed_minutes + let elapsed = ls.last_diaper_since_minutes ?? (ls as any).last_diaper_elapsed_minutes + let elapsedStool = (ls as any).last_stool_elapsed_minutes + + if ((ls as any).last_diaper_iso) { + try { + const t = new Date((ls as any).last_diaper_iso).getTime() + if (!isNaN(t)) elapsed = Math.max(0, Math.floor((nowEpoch.value - t) / 60000)) + } catch {} + } + if ((ls as any).last_stool_iso) { + try { + const t = new Date((ls as any).last_stool_iso).getTime() + if (!isNaN(t)) elapsedStool = Math.max(0, Math.floor((nowEpoch.value - t) / 60000)) + } catch {} + } const formatElapsed = (mins: number | null | undefined) => { if (mins === null || mins === undefined) return null @@ -549,6 +571,50 @@ const currentAgeMonths = computed(() => {

+ +
+
+ 🌙 + Nachtschlaf ({{ data.today.nights.length }} erfasst) +
+
+
+
+
+ 🌙 +
+
+
+ {{ ns.start }} – {{ ns.end || 'schläft gerade' }} Uhr + +
+
+ ({{ ns.location }}) + · {{ ns.note }} +
+
+
+
+ + {{ ns.duration_minutes ? `${ns.duration_minutes} min` : 'in Ausführung' }} + + +
+
+
+
+
diff --git a/projects/mila/src/components/SleepChart.vue b/projects/mila/src/components/SleepChart.vue index 3aedad6..a49f6be 100644 --- a/projects/mila/src/components/SleepChart.vue +++ b/projects/mila/src/components/SleepChart.vue @@ -57,7 +57,10 @@ const bars = computed(() => { let nightHours = 0 let napHours = 0 - if (day.sessions && day.sessions.length > 0) { + if (typeof day.night_minutes === 'number' || typeof day.nap_minutes === 'number') { + nightHours = Math.max(0, (day.night_minutes || 0) / 60) + napHours = Math.max(0, (day.nap_minutes || 0) / 60) + } else if (day.sessions && day.sessions.length > 0) { for (const s of day.sessions) { const mins = s.duration_minutes || 0 if (s.type === 'night') { @@ -71,7 +74,7 @@ const bars = computed(() => { napHours = Math.max(0, day.total_hours - nightHours) } - const totalHours = Math.max(0, day.total_hours) + const totalHours = Math.max(0, day.total_hours, nightHours + napHours) const yTotal = getY(totalHours) const yNight = getY(nightHours) const nightHeight = Math.max(0, (paddingTop + innerHeight) - yNight) @@ -172,6 +175,59 @@ function getMinuteFromTime(timeStr: string): number { return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10) } +interface RasterBlock { + startMin: number + durationMin: number + type: 'night' | 'nap' + label: string + title: string +} + +function getDay24hBlocks(day: HistoryRow, dayIdx: number): RasterBlock[] { + const blocks: RasterBlock[] = [] + + // 1. Check previous calendar day (props.history[dayIdx + 1]) for overnight sleep spilling into 00:00..wake_time + const prevDay = props.history[dayIdx + 1] + if (prevDay?.sessions) { + for (const s of prevDay.sessions) { + if (!s.start) continue + const sMin = getMinuteFromTime(s.start) + const dur = s.duration_minutes || 60 + if (sMin + dur > 1440) { + const morningMin = Math.min(1440, (sMin + dur) - 1440) + if (morningMin > 0) { + blocks.push({ + startMin: 0, + durationMin: morningMin, + type: s.type === 'night' ? 'night' : 'nap', + label: `${morningMin}m`, + title: `Nachtschlaf (Vorabend ${s.start} – ${s.end || 'morgens'}): ${morningMin} min nach 00:00 Uhr (${s.duration_minutes} min gesamt)`, + }) + } + } + } + } + + // 2. Add sessions starting on this calendar day (clamped to 24:00) + if (day.sessions) { + for (const s of day.sessions) { + if (!s.start) continue + const sMin = getMinuteFromTime(s.start) + const dur = s.duration_minutes || 60 + const clampedDur = Math.max(1, Math.min(dur, 1440 - sMin)) + blocks.push({ + startMin: sMin, + durationMin: clampedDur, + type: s.type === 'night' ? 'night' : 'nap', + label: `${clampedDur}m`, + title: `${s.type === 'night' ? 'Nachtschlaf' : 'Nap'}: ${s.start}–${s.end || 'jetzt'} (${s.duration_minutes} min)`, + }) + } + } + + return blocks +} + function handlePointer(event: MouseEvent | TouchEvent) { const svg = (event.currentTarget as HTMLElement).getBoundingClientRect() const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX @@ -474,7 +530,7 @@ function handlePointer(event: MouseEvent | TouchEvent) {
@@ -494,24 +550,22 @@ function handlePointer(event: MouseEvent | TouchEvent) {
-