export type ConversationChannel='say'|'whisper'; export type ConversationRequest={ socialTurn?:0|1; id:string; createdAt:string; speakerName:string; message:string; channel:ConversationChannel; zoneName:string; receiver:string; selectedPlayer:string; nearbyBots:string[]; status:'pending'|'processing'|'awaiting_delivery'|'completed'|'failed'; attempts:number; }; async function ensure(db:D1Database){ await db.prepare(`CREATE TABLE IF NOT EXISTS conversation_requests ( id TEXT PRIMARY KEY NOT NULL, created_at TEXT NOT NULL, speaker_name TEXT NOT NULL, message TEXT NOT NULL, channel TEXT NOT NULL, zone_name TEXT NOT NULL, receiver TEXT NOT NULL, selected_player TEXT NOT NULL, nearby_bots TEXT NOT NULL, status TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, started_at TEXT, completed_at TEXT, result TEXT, error TEXT )`).run(); await db.prepare(`CREATE INDEX IF NOT EXISTS idx_conversation_requests_status_created ON conversation_requests(status,created_at)`).run(); } function text(payload:Record,key:string,max:number){ const value=payload[key]; return typeof value==='string'?value.trim().slice(0,max):''; } export async function queueConversationEvent(db:D1Database,kind:string,payload:Record){ if(payload.isBot===true)return null; if(kind!=='player.chat'&&kind!=='player.whisper')return null; const speakerName=text(payload,'name',40); const message=text(payload,'message',500); if(!speakerName||!message)return null; if(message.startsWith('.')||message.startsWith('!'))return null; if(kind==='player.whisper'&&payload.receiverIsBot!==true)return null; if(kind==='player.chat'&&!text(payload,'selectedPlayer',40)&&!text(payload,'nearbyBots',1000))return null; await ensure(db); const id=crypto.randomUUID(); const createdAt=new Date().toISOString(); const channel:ConversationChannel=kind==='player.whisper'?'whisper':'say'; await db.prepare(`INSERT INTO conversation_requests (id,created_at,speaker_name,message,channel,zone_name,receiver,selected_player,nearby_bots,status,attempts) VALUES (?,?,?,?,?,?,?,?,?,'pending',0)`) .bind( id,createdAt,speakerName,message,channel, text(payload,'zoneName',120), text(payload,'receiver',40), text(payload,'selectedPlayer',40), text(payload,'nearbyBots',1000) ).run(); return {id,createdAt}; } export async function leaseConversation(db:D1Database):Promise{ await ensure(db); const staleBefore=new Date(Date.now()-90_000).toISOString(); await db.prepare(`UPDATE conversation_requests SET status='pending',started_at=NULL WHERE status='processing' AND started_at IS NOT NULL AND started_at(); if(!row)return null; const startedAt=new Date().toISOString(); const updated=await db.prepare(`UPDATE conversation_requests SET status='processing',started_at=?,attempts=attempts+1 WHERE id=? AND status='pending'`).bind(startedAt,row.id).run(); if(updated.meta.changes!==1)return null; return { socialTurn:row.channel==='social-start'?0:row.channel==='social-reply'?1:undefined, id:row.id, createdAt:row.created_at, speakerName:row.speaker_name, message:row.message, channel:row.channel==='whisper'?'whisper':'say', zoneName:row.zone_name, receiver:row.receiver, selectedPlayer:row.selected_player, nearbyBots:String(row.nearby_bots||'').split(',').map((x:string)=>x.trim()).filter(Boolean), status:'processing', attempts:Number(row.attempts||0)+1 }; } export async function completeConversation(db:D1Database,id:string,result:string){ await ensure(db); await db.prepare(`UPDATE conversation_requests SET status='completed',result=?,error=NULL,completed_at=? WHERE id=?`).bind(result.slice(0,2000),new Date().toISOString(),id).run(); } export async function failConversation(db:D1Database,id:string,error:string){ await ensure(db); await db.prepare(`UPDATE conversation_requests SET status='failed',error=?,completed_at=? WHERE id=?`).bind(error.slice(0,2000),new Date().toISOString(),id).run(); } export async function cleanupConversations(db:D1Database){ await ensure(db); const before=new Date(Date.now()-24*60*60_000).toISOString(); await db.prepare(`DELETE FROM conversation_requests WHERE status IN ('completed','failed') AND completed_at IS NOT NULL AND completed_at(); const deliveries=[]; for(const row of rows.results){ const delivery=JSON.parse(row.result) as {request:ConversationRequest;botId:string;reply:string;commandId:string}; const command=await db.prepare('SELECT status,error FROM ale_commands WHERE id=?').bind(delivery.commandId).first<{status:string;error:string|null}>(); deliveries.push({...delivery,status:command?.status??'pending',error:command?.error}); } return deliveries; } // One autonomous exchange per two-minute bucket; human chat keeps priority. export async function queueSocialEncounter(db:D1Database,payload:Record){ const a=text(payload,'socialA',40),b=text(payload,'socialB',40); const at=Number(payload.observedAt); if(!a||!b||a===b||!Number.isFinite(at)||Math.abs(Date.now()/1000-at)>60)return; await ensure(db); const now=new Date().toISOString(); const cutoff=new Date(Date.now()-10*60_000).toISOString(); const id='social-'+Math.floor(Date.now()/120_000); await db.prepare(`INSERT OR IGNORE INTO conversation_requests (id,created_at,speaker_name,message,channel,zone_name,receiver,selected_player,nearby_bots,status,attempts) SELECT ?,?,?,?,'social-start',?,?,?,?,'pending',0 WHERE NOT EXISTS (SELECT 1 FROM conversation_requests WHERE status IN ('pending','processing','awaiting_delivery')) AND NOT EXISTS (SELECT 1 FROM conversation_requests WHERE channel LIKE 'social-%' AND created_at>? AND (speaker_name IN (?,?) OR selected_player IN (?,?)))`) .bind(id,now,b,'Initiate a brief friendly in-world conversation with your nearby companion.',text(payload,'zoneName',120),a,a,b,cutoff,a,b,a,b).run(); } export async function queueSocialReply(db:D1Database,request:ConversationRequest,botName:string,reply:string){ if(request.socialTurn!==0)return; await db.prepare(`INSERT OR IGNORE INTO conversation_requests (id,created_at,speaker_name,message,channel,zone_name,receiver,selected_player,nearby_bots,status,attempts) VALUES (?,?,?,?,'social-reply',?,?,?,?,'pending',0)`) .bind(request.id+':reply',new Date().toISOString(),botName,reply,request.zoneName,request.speakerName,request.speakerName,botName).run(); }