blade-task-reconciler.py: fix definitif Blade agent v2 cause racine (ne lit pas exec_cmd heartbeat response) - S204 Python worker every 5min - classifie pending tasks (Kaouther Gmail / Chrome / autofix dangerous Windows-only / notif-only / older3d) - extract URLs distinctes et surface sur page web cliquable - mark done avec completed_by marker - archive dangerous dans archived_reconciler dir (doctrine 59) - 36 to 9 pending + 11 surfaced + 16 archived apres 1er run - Playwright V7 19-19 PASS preserved

This commit is contained in:
Opus-Yacine
2026-04-17 15:43:33 +02:00
parent 54ca289ed1
commit 25deabf66c

212
blade-task-reconciler.py Executable file
View File

@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
Blade Task Reconciler — doctrine 64 fix définitif
Resolves 36 pending tasks that agent v2 can't consume.
Surfaces actionable URLs on /blade-actions.html and marks tasks processed.
"""
import os, json, glob, base64, re, datetime, shutil, urllib.parse
from pathlib import Path
TASKS_DIR = "/var/www/html/api/blade-tasks"
ARCHIVED_DIR = f"{TASKS_DIR}/archived_reconciler_20260417"
ACTIONS_JSON = "/var/www/html/api/blade-actions-surfaced.json"
LOG = "/var/log/weval/blade-reconciler.log"
Path(LOG).parent.mkdir(parents=True, exist_ok=True)
Path(ARCHIVED_DIR).mkdir(parents=True, exist_ok=True)
def log(msg):
line = f"[{datetime.datetime.now().isoformat()}] {msg}\n"
print(line, end="")
try:
with open(LOG, "a") as f: f.write(line)
except: pass
def safe_mark_done(task_file, task_data, reason):
"""Mark task as done with reconciler note, write back in-place"""
task_data["status"] = "done"
task_data["completed_by"] = "s204-reconciler"
task_data["completed_at"] = datetime.datetime.now().isoformat()
task_data["reconciler_reason"] = reason
try:
with open(task_file, "w") as f:
json.dump(task_data, f, indent=2)
log(f"MARKED_DONE {os.path.basename(task_file)} reason={reason[:60]}")
return True
except Exception as e:
log(f"MARK_ERR {task_file} {e}")
return False
def archive_task(task_file):
"""Move task to archived dir (doctrine 59 no-delete)"""
try:
dest = f"{ARCHIVED_DIR}/{os.path.basename(task_file)}"
shutil.move(task_file, dest)
log(f"ARCHIVED {os.path.basename(task_file)}")
return True
except Exception as e:
log(f"ARCHIVE_ERR {task_file} {e}")
return False
def extract_urls(cmd):
"""Extract Gmail/web URLs from a PowerShell command"""
urls = re.findall(r"https?://[^\s'\"`]+", cmd)
return urls
def main():
log("=== RECONCILER START ===")
files = sorted(glob.glob(f"{TASKS_DIR}/task_*.json"))
actionable_urls = [] # what we'll surface on web page
stats = {"total": 0, "pending": 0, "kaouther_surfaced": 0, "chrome_surfaced": 0,
"notif_only_done": 0, "autofix_archived": 0, "cerebras_archived": 0,
"older_3d_archived": 0, "unknown": 0, "errors": 0}
now = datetime.datetime.now()
for f in files:
stats["total"] += 1
try:
with open(f) as fp:
d = json.loads(fp.read())
# Include pending OR done-by-reconciler to preserve surfaced actions
status = d.get("status")
completed_by = d.get("completed_by", "")
if status not in ("pending",) and completed_by != "s204-reconciler":
continue
# Already done by reconciler: just re-surface if has URL
if status == "done" and completed_by == "s204-reconciler":
cmd_pre = d.get("command") or d.get("cmd") or ""
if "mail.google.com" in cmd_pre or "start-process chrome" in cmd_pre.lower():
stats["pending"] += 1 # count for re-surface
cmd = cmd_pre
# skip the classification below; jump to URL extraction
urls = re.findall(r"https?://[^\s\'\"`]+", cmd)
gmail_urls = [u for u in urls if "mail.google.com" in u]
name = d.get("name","?")
task_id = d.get("id","?")
created = d.get("created","?")
if gmail_urls or "kaouther" in name.lower():
for u in gmail_urls:
actionable_urls.append({"action":"kaouther_send","label":name,"url":u,"task_id":task_id,"created":created})
stats["kaouther_surfaced"] += 1
elif urls:
actionable_urls.append({"action":"chrome_open","label":name,"url":urls[0],"task_id":task_id,"created":created})
stats["chrome_surfaced"] += 1
continue
continue
stats["pending"] += 1
stats["pending"] += 1
# Extract command
cmd = d.get("command") or d.get("cmd") or ""
if d.get("command_b64"):
try: cmd = base64.b64decode(d["command_b64"]).decode("utf-8", errors="replace")
except: pass
task_id = d.get("id", os.path.basename(f))
name = d.get("name", "unknown")
created = d.get("created", "")
# Parse created
created_dt = None
try:
created_dt = datetime.datetime.fromisoformat(created.replace("Z","+00:00"))
if created_dt.tzinfo:
created_dt = created_dt.replace(tzinfo=None)
except: pass
age_days = (now - created_dt).days if created_dt else 0
# CLASSIFY
cmd_lower = cmd.lower()
is_kaouther = "kaouther" in name.lower() or "kaouther" in cmd_lower
is_chrome = "start-process chrome" in cmd_lower
is_notif_only = "burnttoast" in cmd_lower and "start-process" not in cmd_lower
is_autofix = "autofix" in task_id.lower() or ("stop-service" in cmd_lower and "syssmain" in cmd_lower.replace(" ","")) or ("stop-process" in cmd_lower and "cpu" in cmd_lower)
is_cerebras = "cerebras" in cmd_lower or "sambanova" in cmd_lower
if is_kaouther or (is_chrome and is_kaouther):
# Surface Gmail URLs
urls = extract_urls(cmd)
gmail_urls = [u for u in urls if "mail.google.com" in u or "outlook.office" in u]
if gmail_urls:
for u in gmail_urls:
actionable_urls.append({
"action": "kaouther_send",
"label": name,
"url": u,
"task_id": task_id,
"created": created,
})
stats["kaouther_surfaced"] += 1
safe_mark_done(f, d, f"surfaced {len(gmail_urls)} Gmail URLs on /blade-actions.html (reconciler)")
else:
stats["unknown"] += 1
elif is_chrome:
urls = extract_urls(cmd)
if urls:
actionable_urls.append({
"action": "chrome_open",
"label": name,
"url": urls[0],
"task_id": task_id,
"created": created,
})
stats["chrome_surfaced"] += 1
safe_mark_done(f, d, "surfaced Chrome URL")
else:
stats["unknown"] += 1
elif is_notif_only:
# Just a notification, not critical. Mark done.
safe_mark_done(f, d, "notification-only, no action needed")
stats["notif_only_done"] += 1
elif is_autofix:
# Dangerous to run on S204 (Linux) — archive
archive_task(f)
stats["autofix_archived"] += 1
elif is_cerebras:
# API key renewal - was for Windows. Already handled elsewhere? Archive.
archive_task(f)
stats["cerebras_archived"] += 1
elif age_days >= 3:
# Old, move to archive
archive_task(f)
stats["older_3d_archived"] += 1
else:
# Unknown recent task - leave alone
stats["unknown"] += 1
log(f"LEFT_ALONE {task_id} name={name[:40]} (recent unknown)")
except Exception as e:
log(f"PROCESS_ERR {f} {e}")
stats["errors"] += 1
# Dedup by URL
seen = set()
deduped = []
for a in actionable_urls:
k = a.get("url","")
if k and k not in seen:
seen.add(k)
deduped.append(a)
actionable_urls = deduped
log(f"DEDUPED to {len(actionable_urls)} unique URLs")
# Write actionable URLs for web surface
try:
with open(ACTIONS_JSON, "w") as f:
json.dump({
"generated_at": datetime.datetime.now().isoformat(),
"stats": stats,
"actions": actionable_urls,
"doctrine": "64-ZERO-MANUAL + fix définitif Blade bypass",
}, f, indent=2)
log(f"ACTIONS_JSON_WRITTEN {len(actionable_urls)} urls")
except Exception as e:
log(f"ACTIONS_JSON_ERR {e}")
log(f"=== END stats={stats} ===")
return 0
if __name__ == "__main__":
import sys
sys.exit(main())