exportfunctionensureGroupRoom(d){if(!d.store.get('SELECT id FROM ai_rooms WHERE id=?',GROUP_ROOM))d.store.run('INSERT INTO ai_rooms(id, kind, connection_id, title, created_by, created_at, updated_at) VALUES(?,?,?,?,?,?,?)',GROUP_ROOM,'group',null,'통합',null,nowIso(),nowIso());}
exportfunctionensureDirectRoom(d,conn){
constr=d.store.get('SELECT * FROM ai_rooms WHERE kind=\'direct\' AND connection_id=?',conn.id);if(r)returnr;
constid=newId('room_');d.store.run('INSERT INTO ai_rooms(id, kind, connection_id, title, created_by, created_at, updated_at) VALUES(?,?,?,?,?,?,?)',id,'direct',conn.id,null,conn.createdBy||null,nowIso(),nowIso());returnd.store.get('SELECT * FROM ai_rooms WHERE id=?',id);
}
/** 연결 삭제 때: 그 연결의 개별 방·메시지·제안을 지운다(통합 방의 발언은 기록으로 남긴다). */
exportfunctionremoveRoomsOf(d,connectionId){
constrooms=d.store.all('SELECT id FROM ai_rooms WHERE kind=\'direct\' AND connection_id=?',connectionId);
d.store.tx(()=>{for(constrofrooms){d.store.run('DELETE FROM ai_messages WHERE room_id=?',r.id);d.store.run('DELETE FROM ai_actions WHERE room_id=?',r.id);d.store.run('DELETE FROM ai_rooms WHERE id=?',r.id);}d.store.run('UPDATE ai_actions SET status=\'cancelled\', decided_at=? WHERE connection_id=? AND status=\'proposed\'',nowIso(),connectionId);});
}
exportfunctiongetRoom(d,id){returnd.store.get('SELECT * FROM ai_rooms WHERE id=?',id)||null;}
/** 탭 목록: 통합 방이 항상 먼저, 그다음 연결 순서(position)대로 개별 방. connections 는 connections.list(d) 의 공개 객체. */
constr=getRoom(d,id);if(!r)thrownewHttpError(404,'room_not_found','방이 없습니다');if(r.kind==='group')thrownewHttpError(400,'room_fixed','통합 방은 이름을 바꿀 수 없습니다');
constt=String(title||'').trim();if(!t||t.length>60)thrownewHttpError(400,'bad_title','탭 이름은 1~60자여야 합니다');
d.store.run('UPDATE ai_rooms SET title=?, updated_at=? WHERE id=?',t,nowIso(),id);returngetRoom(d,id);
exportfunctionlistMessages(d,roomId,{after=0,limit=500}={}){returnd.store.all('SELECT * FROM ai_messages WHERE room_id=? AND seq>? ORDER BY seq LIMIT ?',roomId,Number(after)||0,Math.min(2000,limit)).map(publicMessage);}
d.store.tx(()=>{constseq=(d.store.get('SELECT COALESCE(MAX(seq),0) AS s FROM ai_messages WHERE room_id=?',roomId).s||0)+1;d.store.run('INSERT INTO ai_messages(id, room_id, seq, role, connection_id, author, content_json, addressed_json, relay_depth, status, usage_json, error, created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)',id,roomId,seq,role,connectionId,author,toJson(redact(content)),addressed?toJson(addressed):null,relayDepth,status,usage?toJson(usage):null,error,nowIso());d.store.run('UPDATE ai_rooms SET updated_at=? WHERE id=?',nowIso(),roomId);});
returnpublicMessage(d.store.get('SELECT * FROM ai_messages WHERE id=?',id));
constr=d.store.get('SELECT * FROM ai_messages WHERE id=?',id);if(!r)returnnull;
d.store.run('UPDATE ai_messages SET content_json=?, status=?, usage_json=?, error=? WHERE id=?',content?toJson(redact(content)):r.content_json,status||r.status,usage?toJson(usage):r.usage_json,error===undefined?r.error:error,id);
returnpublicMessage(d.store.get('SELECT * FROM ai_messages WHERE id=?',id));
}
/** 새 대화: 방의 기록을 비우고 승인 대기 제안은 취소한다. */
exportfunctionresetRoom(d,roomId){d.store.tx(()=>{d.store.run('DELETE FROM ai_messages WHERE room_id=?',roomId);d.store.run('UPDATE ai_actions SET status=\'cancelled\', decided_at=? WHERE room_id=? AND status=\'proposed\'',nowIso(),roomId);d.store.run('UPDATE ai_rooms SET updated_at=? WHERE id=?',nowIso(),roomId);});}