Add native gameplay outcomes, recovery verification, and realm diagnostics
Core tests / core (push) Successful in 40s
Core tests / core (push) Successful in 40s
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import {outcomeChanges,attentionItems,counterKeys,type OutcomeBaseline,type Perf,type Recovery,type Health} from '@/core/realm-outcomes';
|
||||
import {env} from 'cloudflare:workers';
|
||||
import {z} from 'zod';
|
||||
import {D1Store} from '@/core/store';
|
||||
import type {RuntimeConfig} from '@/core/types';
|
||||
import {observeInsights,activityReason,reportWindow,summarize,DAY,type Cursor,type Evidence} from '@/core/realm-insights';
|
||||
const recovery=z.object({id:z.number().int().positive(),guid:z.number().int().positive(),name:z.string().max(64),at:z.number().finite(),detail:z.string().max(255)});
|
||||
const recovery=z.object({id:z.number().int().positive(),guid:z.number().int().positive(),name:z.string().max(64),at:z.number().finite(),detail:z.string().max(255),status:z.enum(['pending','resumed','no_progress','unverified']).optional(),checkedAt:z.number().nonnegative().optional(),result:z.string().max(255).optional()});
|
||||
const count=z.number().int().nonnegative();
|
||||
const outcome=z.object({guid:count,name:z.string().max(64),quests:count,abandoned:count,lootEvents:count,lootItems:count,partyKills:count,rewarded:count});
|
||||
const measurement=z.number().finite().nonnegative();
|
||||
const performance=z.object({at:measurement,enabled:z.boolean(),ticks:measurement,averageTickMs:measurement,rows:z.array(z.object({category:z.string().max(40),name:z.string().max(160),calls:measurement,totalMs:measurement,averageMs:measurement,maxMs:measurement,perTickMs:measurement})).max(20)});
|
||||
async function ensure(){await env.DB!.batch([
|
||||
env.DB!.prepare('CREATE TABLE IF NOT EXISTS insights_native (id INTEGER PRIMARY KEY,payload TEXT)'),
|
||||
env.DB!.prepare('CREATE TABLE IF NOT EXISTS insights_performance (at INTEGER PRIMARY KEY,payload TEXT)'),
|
||||
env.DB!.prepare('CREATE TABLE IF NOT EXISTS insights_cursor (id INTEGER PRIMARY KEY,payload TEXT)'),
|
||||
env.DB!.prepare('CREATE TABLE IF NOT EXISTS insights_evidence (id TEXT PRIMARY KEY,at INTEGER,payload TEXT)'),
|
||||
env.DB!.prepare('CREATE INDEX IF NOT EXISTS insights_evidence_at ON insights_evidence(at)'),
|
||||
@@ -23,29 +30,43 @@ export async function GET(){
|
||||
const row=await env.DB!.prepare('SELECT payload FROM insights_cursor WHERE id=1').first<{payload:string}>();const cursor:Cursor|undefined=row?JSON.parse(row.payload):undefined;
|
||||
const history=await env.DB!.prepare('SELECT payload FROM insights_reports ORDER BY start DESC LIMIT 30').all<{payload:string}>();
|
||||
const recoveries=await env.DB!.prepare('SELECT payload FROM insights_recoveries ORDER BY id DESC LIMIT 100').all<{payload:string}>();const attempts=recoveries.results.map(r=>JSON.parse(r.payload));
|
||||
return Response.json({collectedAt:cursor?.at??null,startedAt:cursor?.startedAt??null,nextReportAt:reportWindow(now)+DAY,current:await report(reportWindow(now),now+1,cursor?.startedAt??now),reports:history.results.map(r=>JSON.parse(r.payload)),activity:s.bots.filter(b=>b.autonomy?.managed&&b.status==='online').map(b=>({name:b.name,zone:b.zone,action:b.lastDecision?.action??'unknown',observedAt:b.world?.at??null,stationarySince:b.world?.stationarySince??null,lastProgressAt:cursor?.bots[b.id]?.lastProgressAt??null,...activityReason(b,now),recoveries:attempts.filter(r=>r.guid===b.autonomy?.characterGuid).slice(0,3).map(({at,detail})=>({at,detail}))}))},{headers:{'Cache-Control':'no-store'}});
|
||||
const nativeRow=await env.DB!.prepare('SELECT payload FROM insights_native WHERE id=1').first<{payload:string}>();const native:OutcomeBaseline|undefined=nativeRow?JSON.parse(nativeRow.payload):undefined;
|
||||
const perfRows=await env.DB!.prepare('SELECT payload FROM insights_performance ORDER BY at DESC LIMIT 144').all<{payload:string}>();const perfHistory:Perf[]=perfRows.results.map(r=>JSON.parse(r.payload)),perf=perfHistory[0]??null;
|
||||
let health:Health[]=[];if(await env.DB!.prepare("SELECT name FROM sqlite_master WHERE name='operations_samples'").first()){const h=await env.DB!.prepare('SELECT payload FROM operations_samples').all<{payload:string}>();health=h.results.map(r=>JSON.parse(r.payload));}
|
||||
const results=await env.DB!.prepare('SELECT payload FROM insights_evidence WHERE at>=? ORDER BY at DESC LIMIT 60').bind(now-DAY).all<{payload:string}>();const recent:Evidence[]=results.results.map(r=>JSON.parse(r.payload));
|
||||
const aggregates=await env.DB!.prepare("SELECT json_extract(payload,'$.kind') AS kind,json_extract(payload,'$.name') AS name,SUM(json_extract(payload,'$.amount')) AS amount FROM insights_evidence WHERE at>=? GROUP BY kind,name").bind(now-DAY).all<{kind:string;name:string;amount:number}>();
|
||||
const totals:Record<string,number>={};for(const e of aggregates.results)totals[e.kind]=(totals[e.kind]??0)+e.amount;
|
||||
const byBot=s.bots.filter(b=>b.autonomy?.managed&&b.status==='online').map(b=>({name:b.name,zone:b.zone,grouped:!!b.world?.groupId&&now-b.world.at<120,groupBots:(b.world?.groupNames??[]).filter(n=>s.bots.some(o=>o.name===n&&o.autonomy?.managed)),totals:Object.fromEntries(counterKeys.map(k=>[k,aggregates.results.filter(e=>e.name===b.name&&e.kind===k).reduce((sum,e)=>sum+e.amount,0)]))}));
|
||||
const publicRecoveries=attempts.map((r:Recovery)=>({name:r.name,at:r.at,detail:r.detail,status:r.status??'unverified',checkedAt:r.checkedAt??null,result:r.result??'No verification recorded'}));
|
||||
const attention=attentionItems(s,now,cursor?.at??null,native?.at??null,perf,attempts,health);
|
||||
return Response.json({attention,outcomes:{at:native?.at??null,startedAt:native?.startedAt??null,windowStart:Math.max(now-DAY,native?.startedAt??now),totals,bots:byBot,recent:recent.slice(0,60)},recoveryResults:publicRecoveries,performance:{latest:perf,history:perfHistory.map(p=>({at:p.at,averageTickMs:p.averageTickMs,ticks:p.ticks})).reverse()},collectedAt:cursor?.at??null,startedAt:cursor?.startedAt??null,nextReportAt:reportWindow(now)+DAY,current:await report(reportWindow(now),now+1,cursor?.startedAt??now),reports:history.results.map(r=>JSON.parse(r.payload)),activity:s.bots.filter(b=>b.autonomy?.managed&&b.status==='online').map(b=>({name:b.name,zone:b.zone,action:b.lastDecision?.action??'unknown',observedAt:b.world?.at??null,stationarySince:b.world?.stationarySince??null,lastProgressAt:cursor?.bots[b.id]?.lastProgressAt??null,...activityReason(b,now),recoveries:attempts.filter(r=>r.guid===b.autonomy?.characterGuid).slice(0,3).map(({at,detail})=>({at,detail}))}))},{headers:{'Cache-Control':'no-store'}});
|
||||
}
|
||||
export async function POST(request:Request){
|
||||
const token=(env as unknown as RuntimeConfig).AUTONOMY_TOKEN;if(!token||request.headers.get('authorization')!==`Bearer ${token}`)return Response.json({error:'Unauthorized'},{status:401});
|
||||
try{
|
||||
const raw=await request.text();if(raw.length>50000)throw new Error('Payload too large');const body=z.object({at:z.number().finite(),recoveries:z.array(recovery).max(100)}).strict().parse(JSON.parse(raw));
|
||||
const raw=await request.text();if(raw.length>200000)throw new Error('Payload too large');const body=z.object({at:z.number().finite(),recoveries:z.array(recovery).max(100),outcomes:z.array(outcome).max(2000).nullable().optional(),performance:performance.nullable().optional()}).strict().parse(JSON.parse(raw));
|
||||
const now=Math.floor(Date.now()/1000);if(Math.abs(now-body.at)>180)throw new Error('Stale sample');await ensure();
|
||||
const gate=await env.DB!.prepare('UPDATE insights_clock SET at=? WHERE id=1 AND at<?').bind(now,now-40).run();if(!gate.meta.changes)return Response.json({ok:true,skipped:true});
|
||||
const previous=await env.DB!.prepare('SELECT payload FROM insights_cursor WHERE id=1').first<{payload:string}>();const old:Cursor|undefined=previous?JSON.parse(previous.payload):undefined;
|
||||
const {cursor,events}=observeInsights(await new D1Store().load(),old,now);
|
||||
const state=await new D1Store().load();const {cursor,events}=observeInsights(state,old,now);
|
||||
let native:OutcomeBaseline|undefined;
|
||||
if(body.outcomes){const before=await env.DB!.prepare('SELECT payload FROM insights_native WHERE id=1').first<{payload:string}>();const safe=body.outcomes.filter(r=>state.bots.some(b=>b.autonomy?.managed&&b.autonomy?.characterGuid===r.guid&&b.name===r.name));const changed=outcomeChanges(safe,before?JSON.parse(before.payload):undefined,now);native=changed.baseline;events.push(...changed.events);}
|
||||
|
||||
const statements=events.map(e=>env.DB!.prepare('INSERT OR IGNORE INTO insights_evidence VALUES (?,?,?)').bind(e.id,e.at,JSON.stringify(e)));
|
||||
for(const r of body.recoveries){
|
||||
if(r.at>now+60)continue;
|
||||
statements.push(env.DB!.prepare('INSERT OR IGNORE INTO insights_recoveries VALUES (?,?)').bind(r.id,JSON.stringify(r)));
|
||||
statements.push(env.DB!.prepare('INSERT INTO insights_recoveries VALUES (?,?) ON CONFLICT(id) DO UPDATE SET payload=excluded.payload').bind(r.id,JSON.stringify(r)));
|
||||
if(r.at>=cursor.startedAt){const e:Evidence={id:'recovery:'+r.id,at:r.at,kind:'recoveries',name:r.name,amount:1,detail:'Native recovery attempted'};statements.push(env.DB!.prepare('INSERT OR IGNORE INTO insights_evidence VALUES (?,?,?)').bind(e.id,e.at,JSON.stringify(e)));}
|
||||
}
|
||||
if(native)statements.push(env.DB!.prepare('INSERT INTO insights_native VALUES (1,?) ON CONFLICT(id) DO UPDATE SET payload=excluded.payload').bind(JSON.stringify(native)));
|
||||
if(body.performance&&body.performance.at<=now+60&&now-body.performance.at<720)statements.push(env.DB!.prepare('INSERT OR REPLACE INTO insights_performance VALUES (?,?)').bind(body.performance.at,JSON.stringify(body.performance)));
|
||||
statements.push(env.DB!.prepare('INSERT INTO insights_cursor VALUES (1,?) ON CONFLICT(id) DO UPDATE SET payload=excluded.payload').bind(JSON.stringify(cursor)),env.DB!.prepare('INSERT OR IGNORE INTO insights_samples VALUES (?)').bind(now));
|
||||
await env.DB!.batch(statements);
|
||||
for(let start=Math.max(reportWindow(cursor.startedAt),reportWindow(now)-30*DAY);start<reportWindow(now);start+=DAY){
|
||||
if(await env.DB!.prepare('SELECT start FROM insights_reports WHERE start=?').bind(start).first())continue;
|
||||
await env.DB!.prepare('INSERT OR IGNORE INTO insights_reports VALUES (?,?)').bind(start,JSON.stringify(await report(start,start+DAY,cursor.startedAt))).run();
|
||||
}
|
||||
await env.DB!.batch([env.DB!.prepare('DELETE FROM insights_evidence WHERE at<?').bind(now-31*DAY),env.DB!.prepare('DELETE FROM insights_samples WHERE at<?').bind(now-31*DAY),env.DB!.prepare('DELETE FROM insights_reports WHERE start<?').bind(reportWindow(now)-30*DAY),env.DB!.prepare('DELETE FROM insights_recoveries WHERE id NOT IN (SELECT id FROM insights_recoveries ORDER BY id DESC LIMIT 100)')]);
|
||||
await env.DB!.batch([env.DB!.prepare('DELETE FROM insights_performance WHERE at NOT IN (SELECT at FROM insights_performance ORDER BY at DESC LIMIT 144)'),env.DB!.prepare('DELETE FROM insights_evidence WHERE at<?').bind(now-31*DAY),env.DB!.prepare('DELETE FROM insights_samples WHERE at<?').bind(now-31*DAY),env.DB!.prepare('DELETE FROM insights_reports WHERE start<?').bind(reportWindow(now)-30*DAY),env.DB!.prepare('DELETE FROM insights_recoveries WHERE id NOT IN (SELECT id FROM insights_recoveries ORDER BY id DESC LIMIT 100)')]);
|
||||
return Response.json({ok:true});
|
||||
}catch(e){return Response.json({error:e instanceof Error?e.message:'Insight collection failed'},{status:400});}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user