Reconcile dashboard online presence against complete live roster

This commit is contained in:
Codex
2026-09-22 16:30:45 -04:00
parent 360f164905
commit ce6b38f3bb
4 changed files with 46 additions and 6 deletions
+23 -5
View File
@@ -72,6 +72,24 @@ function observedHardcoreDeath(s:State,b:Bot,observation:Observation){
export async function applyAuthoritativeAleEvent(store:StateStore,kind:string,payload:Record<string,unknown>){
let s=await store.load();
const revision=s.revision;
// Complete world snapshots own presence once available; historical events
// must never bring disconnected residents back online.
if(kind==='player.snapshot' && payload.roster===true){
const at=payload.observedAt;
const guids=payload.characterGuids;
if(typeof at!=='number'||!Number.isFinite(at)||!Array.isArray(guids)||!guids.every(g=>Number.isInteger(g)&&g>0))throw new Error('Invalid authoritative roster');
if(at<=(s.aleRosterAt??0))return {matched:false,stale:true};
const online=new Set(guids);
for(const bot of s.bots){
if(bot.status==='dead'||bot.status==='retired')continue;
const present=bot.autonomy?.managed && online.has(bot.autonomy.characterGuid);
bot.status=present?'online':'offline';
bot.simulationBand=present?'background':'dormant';
}
s.aleRosterAt=at;
await store.save(s,revision);
return {matched:true,roster:true};
}
if(kind==='bridge.ready'){
let changed=false;
for(const bot of s.bots){
@@ -103,7 +121,7 @@ export async function applyAuthoritativeAleEvent(store:StateStore,kind:string,pa
const isZaku=name.toLowerCase()==='zaku';
b={
id:isZaku?'hero-zaku':`live-${crypto.randomUUID()}`,
name,role,race,level,zone,status:'online',
name,role,race,level,zone,status:s.aleRosterAt?'offline':'online',
tier:isZaku?'hero':'resident',mortal:true,plotArmorHpFloor:isZaku?1:0,
simulationBand:'background',
routine:{wakeHour:isZaku?10:7,sleepHour:isZaku?3:23,preferredZones:[zone]},
@@ -137,19 +155,19 @@ export async function applyAuthoritativeAleEvent(store:StateStore,kind:string,pa
if(zone)b.zone=zone;
if(kind==='player.login'){
b.status='online';
if(!s.aleRosterAt)b.status='online';
b.simulationBand='foreground';
event(s,'lifecycle',`${b.name} logged into the authoritative AzerothCore world.`,b.id);
}else if(kind==='player.logout'){
b.status='offline';
if(!s.aleRosterAt)b.status='offline';
b.simulationBand='dormant';
event(s,'lifecycle',`${b.name} logged out of the authoritative AzerothCore world.`,b.id);
}else if(kind==='player.zone'||kind==='player.map'){
if(b.status==='offline')b.status='online';
if(!s.aleRosterAt&&b.status==='offline')b.status='online';
b.simulationBand='foreground';
event(s,'lifecycle',`${b.name} is now in ${b.zone} (authoritative ALE observation).`,b.id);
}else if(kind==='player.snapshot'){
if(b.status==='offline')b.status='online';
if(!s.aleRosterAt&&b.status==='offline')b.status='online';
if(b.simulationBand==='dormant')b.simulationBand='background';
}else if(kind==='player.level'){
const text=`I reached level ${b.level} while living in ${b.zone}.`;
+1 -1
View File
@@ -18,7 +18,7 @@ export type PopulationSettings = {defaultZoneTarget:number; zoneMax:number; serv
export type BrainRouterSettings = {enabled:boolean; residentModel:string; heroModel:string; backgroundUseLLM:boolean; residentMemoryLimit:number; heroMemoryLimit:number; residentMaxTokens:number; heroMaxTokens:number};
export type WorldTrigger = {id:string; kind:'zone-change'|'interaction'|'time'|'manual'; zone:string; at:string; playerName?:string; detail?:string};
export type PopulationPlan = {id:string; trigger:WorldTrigger; target:number; activeBefore:number; requested:number; capacity:number; selectedBotIds:string[]; rejectedReasons:string[]; createdAt:string};
export type State = {revision:number; bots:Bot[]; homes:Home[]; recipes:Recipe[]; events:RealmEvent[]; sequence:number; settings:{connector:'simulation'|'azerothcore'; provider:'deterministic'|'ollama'|'openai-compatible'; model:string; brains:BrainRouterSettings; hardcore:HardcoreSettings; population:PopulationSettings}; director:{experience:string; zone:string; playerName:string; brief:string}; population:{lastPlan?:PopulationPlan}; operations:Record<string,{started:number; botId:string}>};
export type State = {revision:number; aleRosterAt?:number; bots:Bot[]; homes:Home[]; recipes:Recipe[]; events:RealmEvent[]; sequence:number; settings:{connector:'simulation'|'azerothcore'; provider:'deterministic'|'ollama'|'openai-compatible'; model:string; brains:BrainRouterSettings; hardcore:HardcoreSettings; population:PopulationSettings}; director:{experience:string; zone:string; playerName:string; brief:string}; population:{lastPlan?:PopulationPlan}; operations:Record<string,{started:number; botId:string}>};
export type DeathSignal = {id:string; at:string; cause:string; killer?:string; zone?:string; verified:boolean};
export type Observation = {zone:string; level:number; availableActions:Activity[]; summary:string; alive:boolean; death?:DeathSignal};
export interface GameConnector {id:string; health():Promise<{ok:boolean; detail:string}>; observe(bot:Bot):Promise<Observation>; lifecycle(bot:Bot,action:'spawn'|'pause'|'resume'|'despawn'|'retire',requestId:string):Promise<void>; decide(bot:Bot,decision:Decision,requestId:string):Promise<{summary:string;zone?:string}>;}