#!/usr/bin/env python3 """Local telemetry and bounded terminal-log retention. No character data is deleted.""" import base64,json,os,shutil,subprocess,sys,time,urllib.request from pathlib import Path repo=Path(__file__).resolve().parents[1] lab=Path(os.environ.get('AZEROTHAI_PLAYABLE_ROOT',str(Path.home()/'azerothai-pc-lab'))) def config(path): out={} for line in path.read_text().splitlines(): if '=' in line and not line.startswith('#'): k,v=line.split('=',1);out[k]=v.strip().strip('"').strip("'") return out cfg={**config(lab/'runtime.env'),**config(repo/'.dev.vars')} base='http://127.0.0.1:8787' def send(data): request=urllib.request.Request(base+'/api/operations',data=json.dumps(data).encode(),headers={'Content-Type':'application/json','Authorization':'Bearer '+cfg['AUTONOMY_TOKEN']}) with urllib.request.urlopen(request,timeout=20) as r:assert r.status==200 if len(sys.argv)==3 and sys.argv[1]=='--windows': data=json.loads(base64.b64decode(sys.argv[2]));data['host']='windows';send(data);sys.exit(0) def bytes_in(path): return sum(p.stat().st_size for p in path.rglob('*') if p.is_file()) if path.exists() else 0 def cpu(): v=list(map(int,Path('/proc/stat').read_text().splitlines()[0].split()[1:]));return sum(v[:8]),v[3]+v[4] a=cpu();time.sleep(1);b=cpu();used=100*(1-(b[1]-a[1])/max(1,b[0]-a[0])) mem={line.split(':')[0]:int(line.split()[1])/1024 for line in Path('/proc/meminfo').read_text().splitlines()} model=False try: u=cfg.get('LLM_URL','').rstrip('/')+'/api/tags' with urllib.request.urlopen(u,timeout=5) as r:model=r.status==200 except Exception:pass maintenance=[] # Copy/truncate preserves existing service file descriptors. Three bounded archives. paths=[lab/'autonomy.log',lab/'ale-relay.log',Path.home()/'azerothai-direct-runtime-live.log',lab/'operations.log'] for log in paths: if log.exists() and log.stat().st_size>10*1024*1024: for n in [3,2]: older=Path(str(log)+'.'+str(n-1)) if older.exists():older.replace(str(log)+'.'+str(n)) shutil.copy2(log,str(log)+'.1') with log.open('r+') as f:f.truncate(0) maintenance.append('Rotated '+log.name) # Only acknowledged transport data; never purge pending messages or lifecycle history. try: state=json.load(urllib.request.urlopen(base+'/api/state',timeout=10));days=max(3,min(30,int(state.get('opsRetentionDays',7)))) env=os.environ.copy();env['MYSQL_PWD']=cfg['DOCKER_DB_ROOT_PASSWORD'] def sql(q):return subprocess.check_output(['docker','exec','-e','MYSQL_PWD','ac-database','mysql','-uroot','-N','-B','-e',q],env=env,text=True) # Share the selected mode with the native guard; retain historical retirements. sql("CREATE TABLE IF NOT EXISTS acore_auth.azerothai_realm_rules (id TINYINT PRIMARY KEY,hardcore TINYINT NOT NULL)") hardcore=1 if state.get('settings',{}).get('hardcore',{}).get('enabled',True) else 0 sql(f"INSERT INTO acore_auth.azerothai_realm_rules VALUES (1,{hardcore}) ON DUPLICATE KEY UPDATE hardcore=VALUES(hardcore)") # Reconcile delayed verified deaths by the full native identity. This never # revives an identity, changes a protected resident, or counts a human as a bot. if state.get('settings',{}).get('hardcore',{}).get('enabled'): retired=0 registry={int(line.split('\t')[0]):line for line in sql("SELECT guid,name,account_id,state,died_at FROM acore_auth.azerothai_lifecycle WHERE state<>'dead' AND protected=0 AND LOWER(name)<>'zaku'").splitlines() if line} for bot in state.get('bots',[]): identity=bot.get('autonomy') or {};death=bot.get('death') or {} if bot.get('status')!='dead' or not death.get('verified') or death.get('source')!='azerothcore' or not identity.get('managed'):continue if not bot.get('mortal') or bot.get('plotArmorHpFloor',0)>0 or bot.get('name','').lower()=='zaku':continue guid=identity.get('characterGuid');account=identity.get('accountId') if type(guid) is not int or type(account) is not int or guid<=0 or account<=0:continue before=registry.get(guid,'');fields=before.split('\t') if len(fields)!=5 or fields[1]!=bot['name'] or int(fields[2])!=account:continue name=bot['name'].encode().hex() where=f"guid={guid} AND account_id={account} AND BINARY name=UNHEX('{name}') AND protected=0 AND LOWER(name)<>'zaku' AND state<>'dead'" # Keep an audit of the old registry row and supporting death before changing it. audit=lab/'lifecycle-reconciliation.jsonl' with audit.open('a') as f:f.write(json.dumps({'at':time.time(),'before':before.strip(),'death':death})+'\n') changed=sql("UPDATE acore_auth.azerothai_lifecycle SET state='dead',died_at=COALESCE(died_at,NOW()) WHERE "+where+'; SELECT ROW_COUNT()') retired+=int(changed.strip()) maintenance.append(f'Hardcore registry checked; {retired} delayed deaths reconciled') for table,column in [('azerothai_event_outbox','delivered_at'),('azerothai_result_outbox','delivered_at')]: sql(f'DELETE FROM acore_auth.{table} WHERE {column} IS NOT NULL AND {column}NOW()-INTERVAL 3 MINUTE,'pending','unverified')),COALESCE(UNIX_TIMESTAMP(r.checked_at),0),COALESCE(r.detail,'No verification recorded') FROM acore_auth.azerothai_recovery_log l LEFT JOIN acore_auth.azerothai_recovery_results r ON r.attempt_id=l.id ORDER BY l.id DESC LIMIT 100").splitlines(): fields=line.split('\t') recovery_rows.append(dict(id=int(fields[0]),guid=int(fields[1]),name=fields[2],at=int(fields[3]),detail=fields[4],status=fields[5],checkedAt=int(fields[6]),result=fields[7])) except Exception as e:maintenance.append('Recovery history unavailable: '+type(e).__name__) spec=importlib.util.spec_from_file_location('realm_evidence',repo/'scripts/realm-evidence.py') evidence=importlib.util.module_from_spec(spec);spec.loader.exec_module(evidence) outcomes=None;perf=None try:outcomes=evidence.collect(sql,current) except Exception as e:maintenance.append('Gameplay counters unavailable: '+type(e).__name__) try:perf=evidence.performance() except Exception as e:maintenance.append('Profiler unavailable: '+type(e).__name__) request=urllib.request.Request(base+'/api/realm-insights',data=json.dumps({'at':time.time(),'recoveries':recovery_rows,'outcomes':outcomes,'performance':perf}).encode(),headers={'Content-Type':'application/json','Authorization':'Bearer '+cfg['AUTONOMY_TOKEN']}) with urllib.request.urlopen(request,timeout=20) as response:assert response.status==200 maintenance.append('Realm report sampled') except Exception as e:maintenance.append('Realm report needs attention: '+type(e).__name__) try: backup_status=json.loads((lab/'backups/latest-status.json').read_text()) maintenance.append(('Backup restored and verified: ' if backup_status.get('ok') else 'BACKUP NEEDS ATTENTION: ')+str(backup_status.get('at','unknown'))) except FileNotFoundError:maintenance.append('First backup verification pending') service=subprocess.run(['systemctl','--user','is-active','azerothai-runtime'],capture_output=True,text=True).stdout.strip() send({'host':'ubuntu','at':time.time(),'cpuPercent':max(0,min(100,used)),'memoryTotalMB':mem['MemTotal'],'memoryAvailableMB':mem['MemAvailable'],'diskFreeGB':shutil.disk_usage(repo).free/1024**3,'databaseMB':bytes_in(repo/'.wrangler/state/v3/d1')/1024**2,'logsMB':sum(p.stat().st_size for p in paths if p.exists())/1024**2,'modelAvailable':model,'runtimeService':service,'maintenance':'; '.join(maintenance)[:500]}) print(time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'telemetry and retention complete')