feat: rewrite KimiClaw heartbeat — launchd, sovereignty fixes, dispatch cap

Rewrote kimi-heartbeat.sh with sovereignty-first design:
- Prefer Tailscale (100.x) over public IP for Gitea API calls
- Use $HOME instead of hardcoded /Users/apayne paths
- Remove token file paths from prompts sent to Kimi API
- Add MAX_DISPATCH=5 cap per heartbeat run
- Proper lockfile with stale detection (10min timeout)
- Correct identity separation: timmy-token for labels, kimi_gitea_token for comments
- Covers 4 repos: timmy-home, timmy-config, the-nexus, hermes-agent
- Label lifecycle: assigned-kimi -> kimi-in-progress -> kimi-done
- Failure handling: removes in-progress label so retry is possible

LaunchAgent: ai.timmy.kimi-heartbeat.plist (every 5 minutes)
Zero LLM cost for polling — bash/curl only. Kimi tokens only for actual work.

All Hermes cron jobs removed — they burned Anthropic tokens for polling.
KimiClaw dispatch is now pure infrastructure, no cloud LLM in the loop.
This commit is contained in:
Alexander Whitestone
2026-03-30 17:59:43 -04:00
parent 83e400d4aa
commit ee7f37c5c7
2 changed files with 143 additions and 77 deletions

View File

@@ -1,15 +1,24 @@
{
"tick_id": "20260328_015026",
"timestamp": "2026-03-28T01:50:26.595915+00:00",
"tick_id": "20260330_212052",
"timestamp": "2026-03-30T21:20:52.930215+00:00",
"perception": {
"gitea_alive": true,
"model_health": {
"ollama_running": true,
"models_loaded": [],
"provider": "local-llama.cpp",
"provider_base_url": "http://localhost:8081/v1",
"provider_model": "hermes4:14b",
"local_inference_running": true,
"models_loaded": [
"NousResearch_Hermes-4-14B-Q4_K_M.gguf"
],
"api_responding": true,
"inference_ok": false,
"inference_error": "HTTP Error 404: Not Found",
"timestamp": "2026-03-28T01:50:26.594893+00:00"
"inference_error": "HTTP Error 500: Internal Server Error",
"latest_session": "session_d8c25163-9934-4ab2-9158-ff18a31e30f5.json",
"latest_export": "session_d8c25163-9934-4ab2-9158-ff18a31e30f5.json",
"export_lag_minutes": 0,
"export_fresh": true,
"timestamp": "2026-03-30T21:20:52.929294+00:00"
},
"Timmy_Foundation/the-nexus": {
"open_issues": 1,
@@ -21,7 +30,7 @@
},
"huey_alive": true
},
"previous_tick": "20260328_014026",
"previous_tick": "20260328_015026",
"decision": {
"actions": [],
"severity": "fallback",

View File

@@ -1,110 +1,167 @@
#!/bin/bash
# kimi-heartbeat.sh — Polls Gitea for assigned-kimi tickets, dispatches to OpenClaw
# Run as: bash ~/.timmy/uniwizard/kimi-heartbeat.sh
# Or as a cron: every 5m
# kimi-heartbeat.sh — Polls Gitea for assigned-kimi issues, dispatches to KimiClaw via OpenClaw
# Zero LLM cost for polling — only calls kimi/kimi-code for actual work.
#
# Run manually: bash ~/.timmy/uniwizard/kimi-heartbeat.sh
# Runs via launchd every 5 minutes: ai.timmy.kimi-heartbeat.plist
#
# Workflow for humans:
# 1. Create or open a Gitea issue in any tracked repo
# 2. Add the "assigned-kimi" label
# 3. This script picks it up, dispatches to KimiClaw, posts results back
# 4. Label transitions: assigned-kimi → kimi-in-progress → kimi-done
set -euo pipefail
TOKEN=$(cat /Users/apayne/.timmy/kimi_gitea_token | tr -d '[:space:]')
BASE="http://100.126.61.75:3000/api/v1"
# --- Config ---
TOKEN=$(cat "$HOME/.timmy/kimi_gitea_token" | tr -d '[:space:]')
TIMMY_TOKEN=$(cat "$HOME/.config/gitea/timmy-token" | tr -d '[:space:]')
# Prefer Tailscale (private network) over public IP
if curl -sf --connect-timeout 2 "http://100.126.61.75:3000/api/v1/version" > /dev/null 2>&1; then
BASE="http://100.126.61.75:3000/api/v1"
else
BASE="http://143.198.27.163:3000/api/v1"
fi
LOG="/tmp/kimi-heartbeat.log"
LOCKFILE="/tmp/kimi-heartbeat.lock"
MAX_DISPATCH=5 # Don't overwhelm Kimi with too many parallel tasks
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
REPOS=(
"Timmy_Foundation/timmy-home"
"Timmy_Foundation/timmy-config"
"Timmy_Foundation/the-nexus"
"Timmy_Foundation/hermes-agent"
)
# Find all issues labeled "assigned-kimi" across repos
REPOS=("Timmy_Foundation/timmy-home" "Timmy_Foundation/timmy-config" "Timmy_Foundation/the-nexus")
# --- Helpers ---
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
# Prevent overlapping runs
if [ -f "$LOCKFILE" ]; then
lock_age=$(( $(date +%s) - $(stat -f %m "$LOCKFILE" 2>/dev/null || echo 0) ))
if [ "$lock_age" -lt 600 ]; then
log "SKIP: previous run still active (lock age: ${lock_age}s)"
exit 0
else
log "WARN: stale lock (${lock_age}s), removing"
rm -f "$LOCKFILE"
fi
fi
trap 'rm -f "$LOCKFILE"' EXIT
touch "$LOCKFILE"
dispatched=0
for repo in "${REPOS[@]}"; do
# Get issues with assigned-kimi label but NOT kimi-in-progress or kimi-done
issues=$(curl -s -H "Authorization: token $TOKEN" \
"$BASE/repos/$repo/issues?state=open&labels=assigned-kimi&limit=10" | \
python3 -c "
# Fetch open issues with assigned-kimi label
response=$(curl -sf -H "Authorization: token $TIMMY_TOKEN" \
"$BASE/repos/$repo/issues?state=open&labels=assigned-kimi&limit=20" 2>/dev/null || echo "[]")
# Filter: skip issues that already have kimi-in-progress or kimi-done
issues=$(echo "$response" | python3 -c "
import json, sys
issues = json.load(sys.stdin)
for i in issues:
labels = [l['name'] for l in i.get('labels',[])]
# Skip if already in-progress or done
try:
data = json.loads(sys.stdin.buffer.read())
except:
sys.exit(0)
for i in data:
labels = [l['name'] for l in i.get('labels', [])]
if 'kimi-in-progress' in labels or 'kimi-done' in labels:
continue
body = (i.get('body','') or '')[:500].replace('\n',' ')
print(f\"{i['number']}|{i['title']}|{body}\")
# Pipe-delimited: number|title|body (truncated, newlines removed)
body = (i.get('body', '') or '')[:1500].replace('\n', ' ').replace('|', ' ')
title = i['title'].replace('|', ' ')
print(f\"{i['number']}|{title}|{body}\")
" 2>/dev/null)
if [ -z "$issues" ]; then
continue
fi
[ -z "$issues" ] && continue
while IFS='|' read -r issue_num title body; do
[ -z "$issue_num" ] && continue
log "DISPATCH: $repo #$issue_num$title"
log "FOUND: $repo #$issue_num$title"
# Add kimi-in-progress label
# First get the label ID
label_id=$(curl -s -H "Authorization: token $TOKEN" \
"$BASE/repos/$repo/labels" | \
python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-in-progress']" 2>/dev/null)
# --- Get label IDs for this repo ---
label_json=$(curl -sf -H "Authorization: token $TIMMY_TOKEN" \
"$BASE/repos/$repo/labels" 2>/dev/null || echo "[]")
if [ -n "$label_id" ]; then
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"labels\":[$label_id]}" \
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1
progress_id=$(echo "$label_json" | python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-in-progress']" 2>/dev/null)
done_id=$(echo "$label_json" | python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-done']" 2>/dev/null)
# --- Add kimi-in-progress label ---
if [ -n "$progress_id" ]; then
curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
-d "{\"labels\":[$progress_id]}" \
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
fi
# Post "picking up" comment
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"body\":\"🟠 **Kimi picking up this task** via OpenClaw heartbeat.\\nBackend: kimi/kimi-code\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1
# --- Post pickup comment as KimiClaw ---
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"body\":\"🟠 **KimiClaw picking up this task** via heartbeat.\\nBackend: kimi/kimi-code (Moonshot AI)\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
# Dispatch to OpenClaw
# Build a self-contained prompt from the issue
prompt="You are Timmy, working on $repo issue #$issue_num: $title
log "DISPATCH: $repo #$issue_num to openclaw"
# --- Build the prompt ---
prompt="You are KimiClaw, an AI agent powered by Kimi K2.5 (Moonshot AI).
You are working on Gitea issue #$issue_num in repo $repo.
ISSUE TITLE: $title
ISSUE BODY:
$body
YOUR TASK:
1. Read the issue carefully
2. Do the work described — create files, write code, analyze, review as needed
3. Work in ~/.timmy/uniwizard/ for new files
4. When done, post a summary of what you did as a comment on the Gitea issue
Gitea API: $BASE, token in /Users/apayne/.config/gitea/token
Repo: $repo, Issue: $issue_num
5. Be thorough but practical. Ship working code."
1. Read the issue carefully and do the work described
2. Be thorough — use your full 256K context when needed
3. When done, provide your COMPLETE results as your response (use markdown)
4. Include code, analysis, or deliverables directly in your response
5. If the task requires creating files, describe what to create and provide the full content"
# Fire via openclaw agent (async via background)
# --- Dispatch to OpenClaw (background) ---
(
result=$(openclaw agent --agent main --message "$prompt" --json 2>/dev/null)
status=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','error'))" 2>/dev/null)
result=$(openclaw agent --agent main --message "$prompt" --json 2>/dev/null || echo '{"status":"error"}')
status=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','error'))" 2>/dev/null || echo "error")
if [ "$status" = "ok" ]; then
log "COMPLETED: $repo #$issue_num"
# Swap kimi-in-progress for kimi-done
done_id=$(curl -s -H "Authorization: token $TOKEN" \
"$BASE/repos/$repo/labels" | \
python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-done']" 2>/dev/null)
progress_id=$(curl -s -H "Authorization: token $TOKEN" \
"$BASE/repos/$repo/labels" | \
python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='kimi-in-progress']" 2>/dev/null)
[ -n "$progress_id" ] && curl -s -X DELETE -H "Authorization: token $TOKEN" \
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1
[ -n "$done_id" ] && curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
# Remove kimi-in-progress, add kimi-done
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
[ -n "$done_id" ] && curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
-d "{\"labels\":[$done_id]}" \
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
else
log "FAILED: $repo #$issue_num$status"
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"body\":\"🔴 **Kimi failed on this task.**\\nStatus: $status\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1
log "FAILED: $repo #$issue_numstatus=$status"
# Post failure comment
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"body\":\"🔴 **KimiClaw failed on this task.**\\nStatus: $status\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\\n\\nWill retry on next heartbeat if label is reset to assigned-kimi.\"}" \
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
# Remove kimi-in-progress on failure so it can be retried
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
fi
) &
dispatched=$((dispatched + 1))
log "DISPATCHED: $repo #$issue_num (background PID $!)"
# Don't flood — wait 5s between dispatches
sleep 5
# Enforce dispatch cap
if [ "$dispatched" -ge "$MAX_DISPATCH" ]; then
log "CAPPED: reached $MAX_DISPATCH dispatches, remaining issues deferred to next heartbeat"
break 2 # Break out of both loops
fi
# Stagger dispatches to avoid overwhelming kimi
sleep 3
done <<< "$issues"
done
log "Heartbeat complete. $(date)"
if [ "$dispatched" -eq 0 ]; then
log "Heartbeat: no pending tasks"
else
log "Heartbeat: dispatched $dispatched task(s)"
fi