feat(sleep): cloud-agent reasoning + Telegram delivery to whitelist
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) <noreply@anthropic.com>
This commit is contained in:
parent
883e2ca938
commit
d3955521d1
2 changed files with 146 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
123
hooks/sleep-report-tg.sh
Executable file
123
hooks/sleep-report-tg.sh
Executable file
|
|
@ -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
|
||||
Loading…
Reference in a new issue