import { Database } from 'bun:sqlite'; import { OWNER_AGENT_TYPE } from '../config.js'; import { isValidGroupFolder } from '../group-folder.js'; import type { AgentType, RegisteredGroup, RoomMode } from '../types.js'; import { type RoomModeSource, type RoomRegistrationSnapshot, type StoredRoomSettings, buildRegisteredGroupFromStoredSettings, getStoredRoomRowsFromDatabase, getStoredRoomSettingsRowFromDatabase, inferOwnerAgentTypeFromRegisteredAgentTypes, inferRoomModeFromRegisteredAgentTypes, inferStoredRoomCapabilityTypes, insertStoredRoomSettings, normalizeStoredAgentType, resolveAssignedRoomFolder, resolveStoredRoomCapabilityTypes, resolveStoredRoomRoleAgentPlan, syncRoomRoleOverridesForRoom, updateStoredRoomMetadata, upsertRoomRoleOverride, } from './room-registration.js'; interface StoredRoomModeRow { roomMode: RoomMode; source: RoomModeSource; } export interface StoredRoomSkillOverride { chatJid: string; agentType: AgentType; skillScope: string; skillName: string; enabled: boolean; createdAt: string; updatedAt: string; } export interface StoredRoomSkillOverrideInput { chatJid: string; agentType: AgentType; skillScope: string; skillName: string; enabled: boolean; } export interface AssignRoomInput { name: string; roomMode?: RoomMode; ownerAgentType?: AgentType; reviewerAgentType?: AgentType; arbiterAgentType?: AgentType | null; folder?: string; trigger?: string; requiresTrigger?: boolean; isMain?: boolean; workDir?: string; addedAt?: string; ownerAgentConfig?: RegisteredGroup['agentConfig']; } export function setStoredRoomOwnerAgentTypeForTestsInDatabase( database: Database, chatJid: string, ownerAgentType: AgentType | null, ): void { database .prepare( `UPDATE room_settings SET owner_agent_type = ?, updated_at = ? WHERE chat_jid = ?`, ) .run(ownerAgentType, new Date().toISOString(), chatJid); if (ownerAgentType) { const now = new Date().toISOString(); upsertRoomRoleOverride(database, chatJid, { role: 'owner', agentType: ownerAgentType, createdAt: now, updatedAt: now, }); } } export function deleteStoredRoomSettingsForTestsInDatabase( database: Database, chatJid: string, ): void { database.prepare('DELETE FROM room_settings WHERE chat_jid = ?').run(chatJid); } export function getRegisteredGroupFromDatabase( database: Database, jid: string, agentType?: string, ): (RegisteredGroup & { jid: string }) | undefined { const requestedAgentType = normalizeStoredAgentType(agentType); const stored = getStoredRoomSettingsRowFromDatabase(database, jid); if (!stored) { return undefined; } return buildRegisteredGroupFromStoredSettings( database, stored, requestedAgentType, ); } function seedRoomBindingForTestsInDatabase( database: Database, jid: string, group: RegisteredGroup, ): void { const existingStored = getStoredRoomSettingsRowFromDatabase(database, jid); if (!isValidGroupFolder(group.folder)) { throw new Error(`Invalid group folder "${group.folder}" for JID ${jid}`); } database.transaction(() => { const seededAgentType = group.agentType || 'claude-code'; const agentTypes = new Set( existingStored ? resolveStoredRoomCapabilityTypes(database, existingStored) : [], ); agentTypes.add(seededAgentType); const inferredRoomMode = inferRoomModeFromRegisteredAgentTypes([ ...agentTypes, ]); const roomMode = existingStored?.modeSource === 'explicit' ? existingStored.roomMode : inferredRoomMode; const ownerAgentType = existingStored?.modeSource === 'explicit' && existingStored.ownerAgentType ? existingStored.ownerAgentType : inferOwnerAgentTypeFromRegisteredAgentTypes([...agentTypes]); const snapshot: RoomRegistrationSnapshot = { name: group.name, folder: group.folder, triggerPattern: existingStored?.modeSource === 'explicit' && existingStored.trigger ? existingStored.trigger : (group.trigger ?? ''), requiresTrigger: group.requiresTrigger ?? existingStored?.requiresTrigger ?? false, isMain: group.isMain ?? existingStored?.isMain ?? false, ownerAgentType, workDir: group.workDir ?? existingStored?.workDir ?? null, }; if (existingStored) { updateStoredRoomMetadata(database, jid, snapshot); if (existingStored.modeSource === 'inferred') { upsertStoredRoomModeInDatabase(database, jid, roomMode, 'inferred'); } } else { insertStoredRoomSettings(database, jid, roomMode, 'inferred', snapshot); } syncRoomRoleOverridesForRoom(database, jid, roomMode, ownerAgentType, { ownerAgentConfig: seededAgentType === ownerAgentType ? group.agentConfig : undefined, ownerCreatedAt: seededAgentType === ownerAgentType ? group.added_at : undefined, reviewerAgentConfig: roomMode === 'tribunal' && seededAgentType !== ownerAgentType ? group.agentConfig : undefined, reviewerCreatedAt: roomMode === 'tribunal' && seededAgentType !== ownerAgentType ? group.added_at : undefined, updatedAt: group.added_at, }); })(); } export function setRegisteredGroupForTestsInDatabase( database: Database, jid: string, group: RegisteredGroup, ): void { seedRoomBindingForTestsInDatabase(database, jid, group); } export function assignRoomInDatabase( database: Database, chatJid: string, input: AssignRoomInput, ): (RegisteredGroup & { jid: string }) | undefined { const existing = getStoredRoomSettingsRowFromDatabase(database, chatJid); const roomMode = input.roomMode || existing?.roomMode || 'single'; const ownerAgentType = input.ownerAgentType || existing?.ownerAgentType || OWNER_AGENT_TYPE; const folder = resolveAssignedRoomFolder( database, chatJid, input.name, input.folder, ); const snapshot: RoomRegistrationSnapshot = { name: input.name, folder, triggerPattern: input.trigger ?? existing?.trigger ?? '', requiresTrigger: input.requiresTrigger ?? existing?.requiresTrigger ?? false, isMain: input.isMain ?? existing?.isMain ?? false, ownerAgentType, workDir: input.workDir ?? existing?.workDir ?? null, }; const now = new Date().toISOString(); database.transaction(() => { if (existing) { database .prepare( `UPDATE room_settings SET room_mode = ?, mode_source = 'explicit', name = ?, folder = ?, trigger_pattern = ?, requires_trigger = ?, is_main = ?, owner_agent_type = ?, work_dir = ?, updated_at = ? WHERE chat_jid = ?`, ) .run( roomMode, snapshot.name, snapshot.folder, snapshot.triggerPattern, snapshot.requiresTrigger ? 1 : 0, snapshot.isMain ? 1 : 0, snapshot.ownerAgentType, snapshot.workDir, now, chatJid, ); } else { insertStoredRoomSettings( database, chatJid, roomMode, 'explicit', snapshot, ); } syncRoomRoleOverridesForRoom(database, chatJid, roomMode, ownerAgentType, { ownerAgentConfig: input.ownerAgentConfig, ownerCreatedAt: input.addedAt ?? now, reviewerAgentType: input.reviewerAgentType, arbiterAgentType: input.arbiterAgentType, updatedAt: input.addedAt ?? now, }); })(); return getRegisteredGroupFromDatabase(database, chatJid); } export function updateRegisteredGroupNameInDatabase( database: Database, jid: string, name: string, ): void { const stored = getStoredRoomSettingsRowFromDatabase(database, jid); if (!stored) { return; } updateStoredRoomMetadata( database, jid, buildRoomRegistrationSnapshotFromStoredRoom(database, stored, { name }), ); } export function getAllRoomBindingsFromDatabase( database: Database, agentTypeFilter?: string, ): Record { const result: Record = {}; const requestedAgentType = normalizeStoredAgentType(agentTypeFilter); const storedRows = getStoredRoomRowsFromDatabase(database); for (const stored of storedRows) { const group = buildRegisteredGroupFromStoredSettings( database, stored, requestedAgentType, ); if (!group) continue; const { jid, ...rest } = group; result[jid] = rest; } return result; } export function getRegisteredAgentTypesForJidFromDatabase( database: Database, jid: string, ): AgentType[] { const stored = getStoredRoomSettingsRowFromDatabase(database, jid); return stored ? resolveStoredRoomCapabilityTypes(database, stored) : []; } export function getStoredRoomSettingsFromDatabase( database: Database, chatJid: string, ): StoredRoomSettings | undefined { return getStoredRoomSettingsRowFromDatabase(database, chatJid); } export function getStoredRoomRoleAgentPlanFromDatabase( database: Database, chatJid: string, ): ReturnType | undefined { const stored = getStoredRoomSettingsRowFromDatabase(database, chatJid); return stored ? resolveStoredRoomRoleAgentPlan(database, stored) : undefined; } export function getStoredRoomSkillOverridesFromDatabase( database: Database, chatJid?: string, ): StoredRoomSkillOverride[] { const params: string[] = []; const where = chatJid ? 'WHERE chat_jid = ?' : ''; if (chatJid) params.push(chatJid); let rows: Array<{ chat_jid: string; agent_type: string; skill_scope: string; skill_name: string; enabled: number; created_at: string; updated_at: string; }>; try { rows = database .prepare( `SELECT chat_jid, agent_type, skill_scope, skill_name, enabled, created_at, updated_at FROM room_skill_overrides ${where} ORDER BY chat_jid, agent_type, skill_scope, skill_name`, ) .all(...params) as Array<{ chat_jid: string; agent_type: string; skill_scope: string; skill_name: string; enabled: number; created_at: string; updated_at: string; }>; } catch { return []; } return rows .map((row) => { const agentType = normalizeStoredAgentType(row.agent_type); if (!agentType) return null; return { chatJid: row.chat_jid, agentType, skillScope: row.skill_scope, skillName: row.skill_name, enabled: row.enabled === 1, createdAt: row.created_at, updatedAt: row.updated_at, }; }) .filter((row): row is StoredRoomSkillOverride => Boolean(row)); } export function upsertStoredRoomSkillOverrideInDatabase( database: Database, input: StoredRoomSkillOverrideInput, ): void { const agentType = normalizeStoredAgentType(input.agentType); if (!agentType) { throw new Error(`Unsupported agent type: ${input.agentType}`); } const now = new Date().toISOString(); database .prepare( `INSERT INTO room_skill_overrides ( chat_jid, agent_type, skill_scope, skill_name, enabled, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(chat_jid, agent_type, skill_scope, skill_name) DO UPDATE SET enabled = excluded.enabled, updated_at = excluded.updated_at`, ) .run( input.chatJid, agentType, input.skillScope, input.skillName, input.enabled ? 1 : 0, now, now, ); } export function deleteStoredRoomSkillOverrideFromDatabase( database: Database, input: Omit, ): void { const agentType = normalizeStoredAgentType(input.agentType); if (!agentType) { throw new Error(`Unsupported agent type: ${input.agentType}`); } database .prepare( `DELETE FROM room_skill_overrides WHERE chat_jid = ? AND agent_type = ? AND skill_scope = ? AND skill_name = ?`, ) .run(input.chatJid, agentType, input.skillScope, input.skillName); } function getStoredRoomModeRowFromDatabase( database: Database, chatJid: string, ): StoredRoomModeRow | undefined { const row = getStoredRoomSettingsFromDatabase(database, chatJid); return row ? { roomMode: row.roomMode, source: row.modeSource, } : undefined; } function buildRoomRegistrationSnapshotFromStoredRoom( database: Database, stored: StoredRoomSettings, overrides?: Partial>, ): RoomRegistrationSnapshot { const capabilityTypes = resolveStoredRoomCapabilityTypes(database, stored); const ownerAgentType = stored.ownerAgentType || (capabilityTypes.length > 0 ? inferOwnerAgentTypeFromRegisteredAgentTypes(capabilityTypes) : OWNER_AGENT_TYPE); const name = overrides?.name ?? stored.name ?? stored.chatJid; return { name, folder: resolveAssignedRoomFolder( database, stored.chatJid, name, stored.folder, ), triggerPattern: stored.trigger ?? '', requiresTrigger: stored.requiresTrigger ?? false, isMain: stored.isMain ?? false, ownerAgentType, workDir: stored.workDir ?? null, }; } function upsertStoredRoomModeInDatabase( database: Database, chatJid: string, roomMode: RoomMode, source: RoomModeSource, ): void { database .prepare( `INSERT INTO room_settings ( chat_jid, room_mode, mode_source, updated_at ) VALUES (?, ?, ?, ?) ON CONFLICT(chat_jid) DO UPDATE SET room_mode = excluded.room_mode, mode_source = excluded.mode_source, updated_at = excluded.updated_at`, ) .run(chatJid, roomMode, source, new Date().toISOString()); } export function getExplicitRoomModeFromDatabase( database: Database, chatJid: string, ): RoomMode | undefined { const row = getStoredRoomModeRowFromDatabase(database, chatJid); return row?.source === 'explicit' ? row.roomMode : undefined; } export function setExplicitRoomModeInDatabase( database: Database, chatJid: string, roomMode: RoomMode, ): void { upsertStoredRoomModeInDatabase(database, chatJid, roomMode, 'explicit'); } export function clearExplicitRoomModeInDatabase( database: Database, chatJid: string, ): void { const stored = getStoredRoomSettingsRowFromDatabase(database, chatJid); if (!stored) { return; } const agentTypes = inferStoredRoomCapabilityTypes(database, stored); if (agentTypes.length === 0) { database .prepare('DELETE FROM room_settings WHERE chat_jid = ?') .run(chatJid); database .prepare('DELETE FROM room_role_overrides WHERE chat_jid = ?') .run(chatJid); return; } upsertStoredRoomModeInDatabase( database, chatJid, inferRoomModeFromRegisteredAgentTypes(agentTypes), 'inferred', ); } export function getEffectiveRoomModeFromDatabase( database: Database, chatJid: string, ): RoomMode { return ( getStoredRoomModeRowFromDatabase(database, chatJid)?.roomMode ?? 'single' ); } export function getEffectiveRuntimeRoomModeFromDatabase( database: Database, chatJid: string, ): RoomMode { return ( getStoredRoomSettingsFromDatabase(database, chatJid)?.roomMode ?? 'single' ); }