Files
timmy-home/uniwizard/codeclaw_qwen_heartbeat.py

108 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
import json, os, signal, subprocess, time, urllib.request
from pathlib import Path
BASE = os.environ.get('GITEA_API_BASE', 'https://forge.alexanderwhitestone.com/api/v1')
TIMMY_TOKEN = (Path.home() / '.config' / 'gitea' / 'timmy-token').read_text().strip()
LOG = Path('/tmp/codeclaw-qwen-heartbeat.log')
LOCK = Path('/tmp/codeclaw-qwen-heartbeat.lock')
ACTIVE = Path('/tmp/codeclaw-qwen-active.json')
REPOS = [
'Timmy_Foundation/timmy-home',
'Timmy_Foundation/timmy-config',
'Timmy_Foundation/the-nexus',
'Timmy_Foundation/hermes-agent',
]
headers = {'Authorization': f'token {TIMMY_TOKEN}', 'Accept': 'application/json', 'Content-Type': 'application/json'}
def log(msg):
line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
print(line)
with LOG.open('a') as f:
f.write(line + '\n')
def api(method, path, data=None):
req = urllib.request.Request(f"{BASE}{path}", headers=headers, method=method)
if data is not None:
req.data = json.dumps(data).encode()
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read().decode()
return json.loads(raw) if raw else {}
def process_alive(pid):
try:
os.kill(int(pid), 0)
except (OSError, ProcessLookupError, ValueError, TypeError):
return False
return True
if LOCK.exists() and time.time() - LOCK.stat().st_mtime < 840:
log('SKIP: lock active')
raise SystemExit(0)
LOCK.write_text(str(os.getpid()))
try:
if ACTIVE.exists():
try:
active = json.loads(ACTIVE.read_text())
pid = active.get('pid')
if pid and process_alive(pid):
log(f'SKIP: worker still active for issue #{active.get("issue")}')
raise SystemExit(0)
ACTIVE.unlink(missing_ok=True)
except Exception:
pass
picked = None
for repo in REPOS:
owner, name = repo.split('/')
# candidate issues: assignee claw-code OR assigned-claw-code label
issues = api('GET', f'/repos/{owner}/{name}/issues?state=open&type=issues&limit=50&sort=created')
for i in reversed(issues):
assignees = [a['login'].lower() for a in (i.get('assignees') or [])]
labels = [l['name'] for l in i.get('labels', [])]
if 'claw-code' not in assignees and 'assigned-claw-code' not in labels:
continue
if 'claw-code-done' in labels:
continue
if 'claw-code-in-progress' in labels:
continue
picked = (repo, i['number'], i['title'])
break
if picked:
break
if not picked:
log('Heartbeat: no pending tasks')
raise SystemExit(0)
repo, issue_num, title = picked
owner, name = repo.split('/')
log(f'FOUND: {repo} #{issue_num}{title}')
# add in-progress label if available
labels = api('GET', f'/repos/{owner}/{name}/labels?limit=100')
progress_id = next((l['id'] for l in labels if l['name'] == 'claw-code-in-progress'), None)
if progress_id:
try:
api('POST', f'/repos/{owner}/{name}/issues/{issue_num}/labels', {'labels': [progress_id]})
except Exception:
pass
api('POST', f'/repos/{owner}/{name}/issues/{issue_num}/comments', {
'body': f'🟠 Code Claw (OpenRouter qwen/qwen3.6-plus:free) picking up this issue via 15-minute heartbeat.\n\nTimestamp: {time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}'
})
worker_log = Path(f'/tmp/codeclaw-qwen-worker-{issue_num}.log')
fh = worker_log.open('a')
cmd = ['python3', str(Path.home() / '.timmy' / 'uniwizard' / 'codeclaw_qwen_worker.py'), repo, str(issue_num), title]
p = subprocess.Popen(cmd, stdout=fh, stderr=fh)
ACTIVE.write_text(json.dumps({'pid': p.pid, 'issue': issue_num, 'repo': repo}))
log(f'DISPATCHED: {repo} #{issue_num} (background PID {p.pid})')
finally:
try:
LOCK.unlink()
except Exception:
pass