Files
AzerothAI/core/realm-outcomes.ts
T
2026-09-22 21:18:32 -04:00

38 lines
4.8 KiB
TypeScript

import type {State} from './types';
import {activityReason,type Evidence} from './realm-insights';
export const counterKeys=['quests','abandoned','lootEvents','lootItems','partyKills','rewarded'] as const;
export type NativeOutcome={guid:number;name:string}&Record<typeof counterKeys[number],number>;
export type OutcomeBaseline={at:number;startedAt:number;rows:NativeOutcome[]};
export function outcomeChanges(rows:NativeOutcome[],previous:OutcomeBaseline|undefined,now:number){
const events:Evidence[]=[];
for(const r of rows){const old=previous?.rows.find(b=>b.guid===r.guid);if(!old)continue;
for(const key of counterKeys){const amount=r[key]-old[key];if(amount>0)events.push({id:`outcome:${r.guid}:${key}:${now}:${r[key]}`,at:now,kind:key,name:r.name,amount,detail:({quests:'Quest objectives completed',abandoned:'Quests abandoned',lootEvents:'Loot collections',lootItems:'Items collected',partyKills:'Kills with a nearby bot attacking the same target',rewarded:'New rewarded quest records'})[key]});}
}
return {baseline:{at:now,startedAt:previous?.startedAt??now,rows},events};
}
export type Recovery={id:number;guid:number;name:string;at:number;detail:string;status?:'pending'|'resumed'|'no_progress'|'unverified';checkedAt?:number;result?:string};
export type Perf={at:number;enabled:boolean;ticks:number;averageTickMs:number;rows:{category:string;name:string;calls:number;totalMs:number;averageMs:number;maxMs:number;perTickMs:number}[]};
export type Health={host:string;at:number;memoryAvailableMB:number;memoryTotalMB:number;diskFreeGB:number;cpuPercent:number;modelAvailable?:boolean;runtimeService?:string};
export type Attention={key:string;severity:'warning'|'critical';title:string;detail:string;target:string;name?:string};
export function attentionItems(s:State,now:number,collectedAt:number|null,outcomeAt:number|null,perf:Perf|null,recoveries:Recovery[],health:Health[]){
const items:Attention[]=[];
const add=(key:string,severity:Attention['severity'],title:string,detail:string,target:string,name?:string)=>items.push({key,severity,title,detail,target,name});
if(!collectedAt||now-collectedAt>180)add('collector','warning','Observations are delayed','The minute collector has not delivered a fresh sample.','System Health');
if(!outcomeAt||now-outcomeAt>180)add('outcomes','warning','Gameplay evidence is delayed','Quest and loot counters are unavailable or stale.','Gameplay Results');
if(!perf||!perf.enabled||now-perf.at>720)add('profiler','warning','Bot profiler needs attention','A current enabled profiler sample is not available.','Bot Performance');
for(const host of ['ubuntu','windows']){const h=health.find(v=>v.host===host);if(!h||now-h.at>180){add('host:'+host,'warning',`${host} telemetry delayed`,'Host resource readings are missing or stale.','System Health');continue;}
if(h.memoryAvailableMB<Math.max(256,h.memoryTotalMB*.05))add('memory:'+host,'critical',`${host} memory pressure`,`${Math.round(h.memoryAvailableMB)} MB available. Check host workload before raising bot population.`,'System Health');
if(h.diskFreeGB<4)add('disk:'+host,'critical',`${host} disk space low`,`${h.diskFreeGB.toFixed(1)} GB free.`,'System Health');
if(h.cpuPercent>90)add('cpu:'+host,'warning',`${host} CPU is busy`,'Latest sample exceeds 90%; this alone does not establish sustained overload.','System Health');
if(host==='ubuntu'&&h.modelAvailable===false)add('model','warning','Language model unavailable','Bot conversations may be delayed.','System Health');
}
const online=s.bots.filter(b=>b.autonomy?.managed&&b.status==='online');
if(now-(s.aleRosterAt??0)<120){if(online.length!==50)add('population',online.length>50?'critical':'warning',`${online.length} / 50 bots online`,online.length>50?'The hard cap needs investigation.':'A vacancy may be waiting for its replacement.','Bots');
const alliance=online.filter(b=>b.world?.team===0).length,horde=online.filter(b=>b.world?.team===1).length;
if(alliance!==25||horde!==25)add('factions','warning','Faction balance differs from target',`${alliance} Alliance · ${horde} Horde · target 25 each.`,'World Monitor');}
for(const b of online){const reason=activityReason(b,now);if(['Possible stall','Telemetry delayed'].includes(reason.label))add('bot:'+b.id,'warning',b.name+' · '+reason.label,reason.detail,'Bot Activity',b.name);}
const latest=new Map<number,Recovery>();for(const r of [...recoveries].sort((a,b)=>b.at-a.at))if(!latest.has(r.guid))latest.set(r.guid,r);
for(const r of latest.values())if(r.status==='no_progress'&&now-r.at<3600&&online.some(b=>b.autonomy?.characterGuid===r.guid&&activityReason(b,now).label==='Possible stall'))add('recovery:'+r.id,'warning',r.name+' · recovery needs follow-up','No native movement or level progress was observed within three minutes.','Recovery Results',r.name);
return items.sort((a,b)=>Number(b.severity==='critical')-Number(a.severity==='critical'));
}