diff --git a/uniwizard/kimi-heartbeat.sh b/uniwizard/kimi-heartbeat.sh index 4b69296..e7c172c 100755 --- a/uniwizard/kimi-heartbeat.sh +++ b/uniwizard/kimi-heartbeat.sh @@ -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: