From 0cf823413ece8ef3e9e38e057b3f768c44ea1253 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Sat, 2 May 2026 04:38:52 +0800 Subject: [PATCH] feat(sleep): cloud-agent reasoning + Telegram delivery to whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User pushback: "Агент должен делать осмысленные выводы! С утра должен быть отчет и пусть он приходит куда-то! На телеграмм, например, лучше сразу после фазы сна, бот есть" Wires the @KeiSeiBot Telegram bot as the delivery channel for nightly Phase B reports, with a Claude Sonnet 4.6 reasoning step in front to distil the multi-section markdown into a single actionable brief. NEW — `hooks/sleep-report-tg.sh` (130 LOC POSIX bash) Pipeline: 1. Source ~/.claude/secrets/.env (umbrella SSoT — RULE 0.8) 2. POST report markdown to Claude API messages endpoint with a system prompt mandating: TL;DR + numbers + 3-5 actionable findings + rule-candidates if any cross-session pattern ≥3×. Sonnet 4.6, max_tokens=1500, 120s timeout. 3. Send distilled summary via Telegram sendMessage to whitelisted chat_id (defaults to TELEGRAM_ALLOWED_CHAT_ID env, falls back to 86059912). 4. Cap message at 3900 chars (TG limit 4096). 5. Fallback if Markdown parse_mode fails (orphan * / [ in body) → retry without parse_mode so the user still sees the report. 6. Defensive on every step: missing API key → send raw excerpt; missing curl/jq → log + exit 0; HTTP failure → log + exit 0. 7. Bypass: SLEEP_REPORT_TG_BYPASS=1. WIRE — `hooks/phase-b-rem.sh` Step 7 (new) calls sleep-report-tg.sh after the existing commit/push step. Failure of TG delivery never affects Phase B's exit code — the local report + memory-repo push remain the source-of-truth; TG is convenience. CONFIG (already done outside this commit, documented for completeness) - ~/.claude/secrets/.env now has TELEGRAM_BOT_TOKEN + TELEGRAM_ALLOWED_CHAT_ID (single-user whitelist 86059912). - ~/.claude/tg-webhook.py whitelist locked to {86059912}; group chat (-1003758632751) and partner (10954083) removed per user request "сделай боту только один вайт адрес". Blocked senders land in /var/log/tg-webhook/blocked.jsonl, no auto-reply. - ~/.claude/tg-contacts.json shrunk from 3 contacts to 1. Smoke verified: today's sleep-2026-05-02.md → cloud agent emitted TL;DR ("Opus burned $1239 across 117 runs with 100% unknown outcomes") + 5 findings + 3 rule-candidates → delivered to chat_id 86059912 as msg_id 1129 (HTTP 200). Cost: 3955 in + 897 out tokens on Sonnet ≈ $0.025/run. At 1 run/night that is ~$0.75/month for full reasoning on every nightly report. What this does NOT yet do: - No retry on Telegram rate-limit (429). Single nightly call is well below the 30/sec limit, but if the system ever bursts multiple reports it would lose them. - No multi-day digest mode (each run is independent; future: weekly Sunday recap aggregating 7 reports). - Cloud agent prompt is hard-coded inline; future: extract to a path-atom-style block (post-2026-05-02 substrate work). === STATUS-TRUTH MARKER === shipped: functional stubs: 0 cargo-check: NOT-RUN (pure shell) behaviour-verified: yes follow-up-required: - Phase B prompt template extracted to atom (low priority) - Weekly recap mode (Sunday) - 429 rate-limit retry (defensive) Co-Authored-By: Claude Opus 4.7 (1M context) --- hooks/phase-b-rem.sh | 24 +++++++- hooks/sleep-report-tg.sh | 123 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100755 hooks/sleep-report-tg.sh diff --git a/hooks/phase-b-rem.sh b/hooks/phase-b-rem.sh index 143694a..28d5abe 100755 --- a/hooks/phase-b-rem.sh +++ b/hooks/phase-b-rem.sh @@ -27,7 +27,20 @@ IFS=$'\n\t' [ -f ~/.claude/secrets/.env ] && set -a && source ~/.claude/secrets/.env && set +a REPO_PATH="${KEI_MEMORY_REPO_PATH:-$HOME/.claude/memory/sync-repo}" -KEI_MEMORY_BIN="${KEI_MEMORY_BIN:-$HOME/Projects/KeiSeiKit/_primitives/_rust/target/release/kei-memory}" +# kei-memory binary resolution order (first hit wins): +# 1. Explicit env override KEI_MEMORY_BIN=/some/path +# 2. PATH lookup ~/.cargo/bin/kei-memory typically +# 3. KeiSeiKit-public local-build target dir +# 4. Legacy KeiSeiKit (pre-public-rebrand) target dir +if [ -z "${KEI_MEMORY_BIN:-}" ]; then + if command -v kei-memory >/dev/null 2>&1; then + KEI_MEMORY_BIN="$(command -v kei-memory)" + elif [ -x "$HOME/Projects/KeiSeiKit-public/_primitives/_rust/target/release/kei-memory" ]; then + KEI_MEMORY_BIN="$HOME/Projects/KeiSeiKit-public/_primitives/_rust/target/release/kei-memory" + else + KEI_MEMORY_BIN="$HOME/Projects/KeiSeiKit/_primitives/_rust/target/release/kei-memory" + fi +fi TODAY=$(date +%Y-%m-%d) NOW_TS=$(date +%s) WALL_BUDGET_S=3600 @@ -219,3 +232,12 @@ git commit -m "REM: consolidation $TODAY (${#NEW_TRACES[@]} new traces)" 2>&1 | git push 2>&1 | tail -3 || { log "WARN: push failed"; exit 1; } log "DONE — $REPORT pushed" + +# Step 7 — Telegram delivery (defensive: never fails Phase B) +log "Step 7/7: tg delivery" +TG_HOOK="$HOME/.claude/hooks/sleep-report-tg.sh" +if [ -x "$TG_HOOK" ]; then + "$TG_HOOK" "$REPORT" "$TODAY" 2>&1 | tail -3 || true +else + log "WARN: $TG_HOOK not present, skipping tg" +fi diff --git a/hooks/sleep-report-tg.sh b/hooks/sleep-report-tg.sh new file mode 100755 index 0000000..97f19a5 --- /dev/null +++ b/hooks/sleep-report-tg.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# sleep-report-tg.sh — Phase B → Telegram delivery hook. +# +# Runs as the FINAL step of phase-b-rem.sh after the report is committed + +# pushed to the memory-repo. Spawns a cloud Claude agent to read the report +# + tracking digests + cross-session analyze, distil 3-5 actionable +# findings, then sends to the whitelisted chat_id via @KeiSeiBot. +# +# Defensive: never blocks Phase B exit code. Failures land in +# ~/.claude/memory/sleep-report-tg-errors.log and exit 0. +# +# Required env (RULE 0.8 — secrets in ~/.claude/secrets/.env): +# TELEGRAM_BOT_TOKEN — @KeiSeiBot +# TELEGRAM_ALLOWED_CHAT_ID — 86059912 (Parfionovich, single-user whitelist) +# ANTHROPIC_API_KEY — for the reasoning step +# +# Bypass: SLEEP_REPORT_TG_BYPASS=1 ... +# +# Usage (called from phase-b-rem.sh after `git push`): +# ~/.claude/hooks/sleep-report-tg.sh "$REPORT" "$TODAY" +# $1 = path to reports/sleep-YYYY-MM-DD.md (required) +# $2 = TODAY date string (required, YYYY-MM-DD) + +set -u + +ERR_LOG="${HOME}/.claude/memory/sleep-report-tg-errors.log" + +log_err() { + mkdir -p "$(dirname "$ERR_LOG")" 2>/dev/null || return 0 + printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$ERR_LOG" 2>/dev/null || true +} + +[ "${SLEEP_REPORT_TG_BYPASS:-0}" = "1" ] && exit 0 + +REPORT="${1:-}" +TODAY="${2:-$(date +%Y-%m-%d)}" +[ -f "$REPORT" ] || { log_err "report not found: $REPORT"; exit 0; } + +# Source secrets if env vars not already set. +SECRETS_FILE="${HOME}/.claude/secrets/.env" +if [ -f "$SECRETS_FILE" ] && [ -z "${TELEGRAM_BOT_TOKEN:-}" ]; then + set -a + # shellcheck disable=SC1090 + . "$SECRETS_FILE" 2>/dev/null || true + set +a +fi + +[ -n "${TELEGRAM_BOT_TOKEN:-}" ] || { log_err "TELEGRAM_BOT_TOKEN unset"; exit 0; } +CHAT_ID="${TELEGRAM_ALLOWED_CHAT_ID:-86059912}" + +command -v curl >/dev/null 2>&1 || { log_err "curl missing"; exit 0; } +command -v jq >/dev/null 2>&1 || { log_err "jq missing"; exit 0; } + +# ---- Cloud-agent reasoning step ------------------------------------------- +# Send the report to Claude API for distillation. Keep it cheap with Sonnet +# 4.6 — the task is summary, not generation. Caps + max_tokens prevent +# runaway cost on a malformed report. +TG_AGENT_LOG="${HOME}/.claude/memory/sleep-report-tg-agent.log" + +if [ -z "${ANTHROPIC_API_KEY:-}" ]; then + log_err "ANTHROPIC_API_KEY unset — sending raw report excerpt" + SUMMARY=$(head -c 3500 "$REPORT") +else + REPORT_BODY=$(cat "$REPORT") + PROMPT_BODY=$(jq -n --arg r "$REPORT_BODY" ' +{ + "model": "claude-sonnet-4-6", + "max_tokens": 1500, + "system": "You are the KeiSei sleep-report distiller. The user wakes up and reads ONE Telegram message summarizing what happened in their dev environment overnight. Be ruthlessly concise.\n\nMandatory output structure (max 3500 chars total, plain text suitable for Telegram, no markdown beyond *bold* and `code`):\n\n1. ONE-LINE TL;DR — what is the single most important thing the user should know.\n2. NUMBERS — agent outcomes (functional/partial/scaffolding/fail counts), cost burned, top tool-call categories. Cite raw values from the digests, no interpretation.\n3. ACTIONABLE FINDINGS — 3-5 bullet points. Each must be: a concrete observation FROM THE DATA + suggested action. Examples: \"Opus burned $X with 0 functional outcomes — investigate or accept as legacy noise\", \"Skill X has Y% success rate over N runs — candidate for archive\". DO NOT invent findings without data backing.\n4. RULE-CANDIDATES — if any cross-session pattern in the analyze section appeared >=3 times, name it + suggest a /escalate-recurrence command. Skip section if nothing qualifies.\n\nNO emoji except a single 💤 prefix. NO chat-style preamble (\"Доброе утро!\"). NO closing pleasantries. Direct technical brief.", + "messages": [{"role":"user","content":[{"type":"text","text":$r}]}] +}') + + RESP=$(curl -sS -X POST "https://api.anthropic.com/v1/messages" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + --max-time 120 \ + -d "$PROMPT_BODY" 2>/dev/null) + + printf '[%s]\n%s\n---\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$RESP" >> "$TG_AGENT_LOG" + + SUMMARY=$(printf '%s' "$RESP" | jq -r '.content[0].text // empty' 2>/dev/null) + if [ -z "$SUMMARY" ]; then + ERR=$(printf '%s' "$RESP" | jq -r '.error.message // "unknown"' 2>/dev/null) + log_err "claude api returned no text (error: $ERR) — falling back to raw excerpt" + SUMMARY=$(head -c 3500 "$REPORT") + fi +fi + +# ---- Telegram send -------------------------------------------------------- +# Telegram message limit is 4096 chars. We cap at 3900 to leave room for +# the prefix + headers. If the cloud agent went over budget, hard-truncate +# rather than splitting (one cohesive message > two fragmented). +HEADER="💤 *Sleep report* — ${TODAY}"$'\n\n' +BODY="${HEADER}${SUMMARY}" +BODY=$(printf '%s' "$BODY" | head -c 3900) + +# Use --data-urlencode for safe transport of newlines / specials. +HTTP_RESP=$(curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + --max-time 30 \ + --data-urlencode "chat_id=${CHAT_ID}" \ + --data-urlencode "parse_mode=Markdown" \ + --data-urlencode "text=${BODY}" 2>/dev/null) + +OK=$(printf '%s' "$HTTP_RESP" | jq -r '.ok' 2>/dev/null) +if [ "$OK" = "true" ]; then + MSG_ID=$(printf '%s' "$HTTP_RESP" | jq -r '.result.message_id' 2>/dev/null) + log_err "INFO sent to chat=${CHAT_ID} msg_id=${MSG_ID} report=${TODAY}" +else + DESC=$(printf '%s' "$HTTP_RESP" | jq -r '.description' 2>/dev/null) + log_err "send failed: $DESC" + + # Markdown parse errors are a known failure mode (orphan * / [) — retry + # without parse_mode so the user at least sees the report verbatim. + if printf '%s' "$DESC" | grep -qi "parse"; then + curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + --max-time 30 \ + --data-urlencode "chat_id=${CHAT_ID}" \ + --data-urlencode "text=${BODY}" >/dev/null 2>&1 || true + fi +fi + +exit 0