feat: rewrite KimiClaw heartbeat — launchd, sovereignty fixes, dispatch cap #112
@@ -3,7 +3,7 @@
|
||||
# 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
|
||||
# Runs via launchd every 2 minutes: ai.timmy.kimi-heartbeat.plist
|
||||
#
|
||||
# Workflow for humans:
|
||||
# 1. Create or open a Gitea issue in any tracked repo
|
||||
@@ -29,10 +29,11 @@ else
|
||||
fi
|
||||
LOG="/tmp/kimi-heartbeat.log"
|
||||
LOCKFILE="/tmp/kimi-heartbeat.lock"
|
||||
MAX_DISPATCH=5 # Don't overwhelm Kimi with too many parallel tasks
|
||||
MAX_DISPATCH=10 # Increased max dispatch to 10
|
||||
PLAN_TIMEOUT=120 # 2 minutes for planning pass
|
||||
EXEC_TIMEOUT=480 # 8 minutes for execution pass
|
||||
BODY_COMPLEXITY_THRESHOLD=500 # chars — above this triggers planning
|
||||
STALE_PROGRESS_SECONDS=3600 # reclaim kimi-in-progress after 1 hour of silence
|
||||
|
||||
REPOS=(
|
||||
"Timmy_Foundation/timmy-home"
|
||||
@@ -65,30 +66,53 @@ for repo in "${REPOS[@]}"; do
|
||||
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
|
||||
# Filter: skip done tasks, but reclaim stale kimi-in-progress work automatically
|
||||
issues=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
import json, sys, datetime
|
||||
STALE = int(${STALE_PROGRESS_SECONDS})
|
||||
|
||||
def parse_ts(value):
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return datetime.datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(sys.stdin.buffer.read())
|
||||
except:
|
||||
sys.exit(0)
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
for i in data:
|
||||
labels = [l['name'] for l in i.get('labels', [])]
|
||||
if 'kimi-in-progress' in labels or 'kimi-done' in labels:
|
||||
if 'kimi-done' in labels:
|
||||
continue
|
||||
# Pipe-delimited: number|title|body_length|body (truncated, newlines removed)
|
||||
|
||||
reclaim = False
|
||||
updated_at = i.get('updated_at', '') or ''
|
||||
if 'kimi-in-progress' in labels:
|
||||
ts = parse_ts(updated_at)
|
||||
age = (now - ts).total_seconds() if ts else (STALE + 1)
|
||||
if age < STALE:
|
||||
continue
|
||||
reclaim = True
|
||||
|
||||
body = (i.get('body', '') or '')
|
||||
body_len = len(body)
|
||||
body_clean = body[:1500].replace('\n', ' ').replace('|', ' ')
|
||||
title = i['title'].replace('|', ' ')
|
||||
print(f\"{i['number']}|{title}|{body_len}|{body_clean}\")
|
||||
updated_clean = updated_at.replace('|', ' ')
|
||||
reclaim_flag = 'reclaim' if reclaim else 'fresh'
|
||||
print(f\"{i['number']}|{title}|{body_len}|{reclaim_flag}|{updated_clean}|{body_clean}\")
|
||||
" 2>/dev/null)
|
||||
|
||||
[ -z "$issues" ] && continue
|
||||
|
||||
while IFS='|' read -r issue_num title body_len body; do
|
||||
while IFS='|' read -r issue_num title body_len reclaim_flag updated_at body; do
|
||||
[ -z "$issue_num" ] && continue
|
||||
log "FOUND: $repo #$issue_num — $title (body: ${body_len} chars)"
|
||||
log "FOUND: $repo #$issue_num — $title (body: ${body_len} chars, mode: ${reclaim_flag}, updated: ${updated_at})"
|
||||
|
||||
# --- Get label IDs for this repo ---
|
||||
label_json=$(curl -sf -H "Authorization: token $TIMMY_TOKEN" \
|
||||
@@ -98,6 +122,15 @@ for i in data:
|
||||
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)
|
||||
kimi_id=$(echo "$label_json" | python3 -c "import json,sys; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='assigned-kimi']" 2>/dev/null)
|
||||
|
||||
if [ "$reclaim_flag" = "reclaim" ]; then
|
||||
log "RECLAIM: $repo #$issue_num — stale kimi-in-progress since $updated_at"
|
||||
[ -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
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"body\":\"🟡 **KimiClaw reclaiming stale task.**\\nPrevious kimi-in-progress state exceeded ${STALE_PROGRESS_SECONDS}s without resolution.\\nLast update: $updated_at\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# --- Add kimi-in-progress label ---
|
||||
if [ -n "$progress_id" ]; then
|
||||
curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
||||
@@ -121,32 +154,11 @@ for i in data:
|
||||
-d "{\"body\":\"🟠 **KimiClaw picking up this task** via heartbeat.\\nBackend: kimi/kimi-code (Moonshot AI)\\nMode: **Planning first** (task is complex)\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
||||
|
||||
plan_prompt="You are KimiClaw, a planning agent. You have 2 MINUTES.
|
||||
plan_prompt="You are KimiClaw, a planning agent. You have 2 MINUTES.\n\nTASK: Analyze this Gitea issue and decide if you can complete it in under 8 minutes, or if it needs to be broken into subtasks.\n\nISSUE #$issue_num in $repo: $title\n\nBODY:\n$body\n\nRULES:\n- If you CAN complete this in one pass (research, write analysis, answer a question): respond with EXECUTE followed by a one-line plan.\n- If the task is TOO BIG (needs git operations, multiple repos, >2000 words of output, or multi-step implementation): respond with DECOMPOSE followed by a numbered list of 2-5 smaller subtasks. Each subtask must be completable in under 8 minutes by itself.\n- Each subtask line format: SUBTASK: <title> | <one-line description>\n- Be realistic about what fits in 8 minutes with no terminal access.\n- You CANNOT clone repos, run git, or execute code. You CAN research, analyze, write specs, review code via API, and produce documents.\n\nRespond with ONLY your decision. No preamble."
|
||||
|
||||
TASK: Analyze this Gitea issue and decide if you can complete it in under 8 minutes, or if it needs to be broken into subtasks.
|
||||
|
||||
ISSUE #$issue_num in $repo: $title
|
||||
|
||||
BODY:
|
||||
$body
|
||||
|
||||
RULES:
|
||||
- If you CAN complete this in one pass (research, write analysis, answer a question): respond with EXECUTE followed by a one-line plan.
|
||||
- If the task is TOO BIG (needs git operations, multiple repos, >2000 words of output, or multi-step implementation): respond with DECOMPOSE followed by a numbered list of 2-5 smaller subtasks. Each subtask must be completable in under 8 minutes by itself.
|
||||
- Each subtask line format: SUBTASK: <title> | <one-line description>
|
||||
- Be realistic about what fits in 8 minutes with no terminal access.
|
||||
- You CANNOT clone repos, run git, or execute code. You CAN research, analyze, write specs, review code via API, and produce documents.
|
||||
|
||||
Respond with ONLY your decision. No preamble."
|
||||
|
||||
plan_result=$(openclaw agent --agent main --message "$plan_prompt" --timeout $PLAN_TIMEOUT --json 2>/dev/null || echo '{"status":"error"}')
|
||||
plan_result=$(openclaw agent --agent main --message "$plan_prompt" --timeout $PLAN_TIMEOUT --json 2>/dev/null || echo '{\"status\":\"error\"}')
|
||||
plan_status=$(echo "$plan_result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','error'))" 2>/dev/null || echo "error")
|
||||
plan_text=$(echo "$plan_result" | python3 -c "
|
||||
import json,sys
|
||||
d = json.load(sys.stdin)
|
||||
payloads = d.get('result',{}).get('payloads',[])
|
||||
print(payloads[0]['text'] if payloads else '')
|
||||
" 2>/dev/null || echo "")
|
||||
plan_text=$(echo "$plan_result" | python3 -c "\nimport json,sys\nd = json.load(sys.stdin)\npayloads = d.get('result',{}).get('payloads',[])\nprint(payloads[0]['text'] if payloads else '')\n" 2>/dev/null || echo "")
|
||||
|
||||
if echo "$plan_text" | grep -qi "^DECOMPOSE"; then
|
||||
# --- Create subtask issues ---
|
||||
@@ -155,7 +167,7 @@ print(payloads[0]['text'] if payloads else '')
|
||||
# Post the plan as a comment
|
||||
escaped_plan=$(echo "$plan_text" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null)
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"body\":\"📋 **Planning complete — decomposing into subtasks:**\\n\\n$plan_text\"}" \
|
||||
-d "{\"body\":\"📝 **Planning complete — decomposing into subtasks:**\\n\\n$plan_text\"}" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
||||
|
||||
# Extract SUBTASK lines and create child issues
|
||||
@@ -250,7 +262,7 @@ print(payloads[0]['text'][:3000] if payloads else 'No response')
|
||||
# Post result as comment (escape for JSON)
|
||||
escaped=$(echo "$response_text" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read())[1:-1])" 2>/dev/null)
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"body\":\"✅ **KimiClaw result:**\\n\\n$escaped\"}" \
|
||||
-d "{\"body\":\" **KimiClaw result:**\\n\\n$escaped\"}" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
||||
|
||||
# Remove kimi-in-progress, add kimi-done
|
||||
@@ -263,7 +275,7 @@ print(payloads[0]['text'][:3000] if payloads else 'No response')
|
||||
log "FAILED: $repo #$issue_num — status=$status"
|
||||
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"body\":\"🔴 **KimiClaw failed/timed out.**\\nStatus: $status\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\\n\\nTask may be too complex for single-pass execution. Consider breaking into smaller subtasks.\"}" \
|
||||
-d "{\"body\":\"\ud83d\udd34 **KimiClaw failed/timed out.**\\nStatus: $status\\nTimestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')\\n\\nTask may be too complex for single-pass execution. Consider breaking into smaller subtasks.\"}" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
||||
|
||||
# Remove kimi-in-progress on failure
|
||||
|
||||
Reference in New Issue
Block a user