diff --git a/src/db.ts b/src/db.ts index 4668c34..706d50e 100644 --- a/src/db.ts +++ b/src/db.ts @@ -913,6 +913,22 @@ export function _initTestDatabaseFromFile(dbPath: string): void { createSchema(db); } +/** @internal - for tests only. */ +export function _setStoredRoomOwnerAgentTypeForTests( + chatJid: string, + ownerAgentType: AgentType | null, +): void { + if (!db) { + throw new Error('Database not initialized'); + } + db.prepare( + `UPDATE room_settings + SET owner_agent_type = ?, + updated_at = ? + WHERE chat_jid = ?`, + ).run(ownerAgentType, new Date().toISOString(), chatJid); +} + /** * Store chat metadata only (no message content). * Used for all chats to enable group discovery without storing sensitive content. diff --git a/src/service-routing.test.ts b/src/service-routing.test.ts index cc918bf..0e6e766 100644 --- a/src/service-routing.test.ts +++ b/src/service-routing.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { + _setStoredRoomOwnerAgentTypeForTests, _initTestDatabase, setExplicitRoomMode, setRegisteredGroup, @@ -108,6 +109,53 @@ describe('service-routing global failover', () => { }); }); + it('uses stored owner agent type when room_settings selects a different owner', () => { + setRegisteredGroup('dc:stored-owner-claude', { + name: 'Stored Owner Claude', + folder: 'stored-owner-claude', + trigger: '@Andy', + added_at: '2024-01-01T00:00:00.000Z', + agentType: 'claude-code', + }); + setRegisteredGroup('dc:stored-owner-claude', { + name: 'Stored Owner Claude', + folder: 'stored-owner-claude', + trigger: '@Codex', + added_at: '2024-01-01T00:00:00.000Z', + agentType: 'codex', + }); + setExplicitRoomMode('dc:stored-owner-claude', 'single'); + _setStoredRoomOwnerAgentTypeForTests( + 'dc:stored-owner-claude', + 'claude-code', + ); + + expect(getEffectiveChannelLease('dc:stored-owner-claude')).toMatchObject({ + chat_jid: 'dc:stored-owner-claude', + owner_service_id: 'claude', + reviewer_service_id: null, + explicit: false, + }); + }); + + it('falls back to the available service when stored owner agent type is unavailable', () => { + setRegisteredGroup('dc:stored-owner-fallback', { + name: 'Stored Owner Fallback', + folder: 'stored-owner-fallback', + trigger: '@Andy', + added_at: '2024-01-01T00:00:00.000Z', + agentType: 'claude-code', + }); + _setStoredRoomOwnerAgentTypeForTests('dc:stored-owner-fallback', 'codex'); + + expect(getEffectiveChannelLease('dc:stored-owner-fallback')).toMatchObject({ + chat_jid: 'dc:stored-owner-fallback', + owner_service_id: 'claude', + reviewer_service_id: null, + explicit: false, + }); + }); + it('creates a same-service reviewer lease when explicit tribunal is runnable on a solo registration', () => { setRegisteredGroup('dc:explicit-tribunal', { name: 'Explicit Tribunal Claude', @@ -145,4 +193,13 @@ describe('service-routing global failover', () => { explicit: false, }); }); + + it('keeps the legacy claude fallback for chats without room settings', () => { + expect(getEffectiveChannelLease('dc:unregistered')).toMatchObject({ + chat_jid: 'dc:unregistered', + owner_service_id: 'claude', + reviewer_service_id: null, + explicit: false, + }); + }); }); diff --git a/src/service-routing.ts b/src/service-routing.ts index d1b8790..9194228 100644 --- a/src/service-routing.ts +++ b/src/service-routing.ts @@ -14,10 +14,12 @@ import { getAllChannelOwnerLeases, getEffectiveRuntimeRoomMode, getRegisteredAgentTypesForJid, + getStoredRoomSettings, setChannelOwnerLease, type ChannelOwnerLeaseRow, } from './db.js'; import { logger } from './logger.js'; +import type { AgentType } from './types.js'; export interface EffectiveChannelLease { chat_jid: string; @@ -55,21 +57,44 @@ function normalizeLeaseRow( }; } -function getDefaultLease(chatJid: string): EffectiveChannelLease { +function getServiceIdForAgentType(agentType: AgentType): string { + return agentType === 'codex' ? CODEX_MAIN_SERVICE_ID : CLAUDE_SERVICE_ID; +} + +function inferFallbackOwnerAgentType( + hasClaude: boolean, + hasCodex: boolean, +): AgentType | undefined { + if (hasClaude && hasCodex) return OWNER_AGENT_TYPE; + if (hasCodex) return 'codex'; + if (hasClaude) return 'claude-code'; + return undefined; +} + +function resolveDefaultOwnerAgentType(chatJid: string): AgentType | undefined { const types = getRegisteredAgentTypesForJid(chatJid); const hasClaude = types.includes('claude-code'); const hasCodex = types.includes('codex'); + const storedOwnerAgentType = getStoredRoomSettings(chatJid)?.ownerAgentType; + + if (storedOwnerAgentType === 'claude-code' && hasClaude) { + return 'claude-code'; + } + if (storedOwnerAgentType === 'codex' && hasCodex) { + return 'codex'; + } + + return inferFallbackOwnerAgentType(hasClaude, hasCodex); +} + +function getDefaultLease(chatJid: string): EffectiveChannelLease { const roomMode = getEffectiveRuntimeRoomMode(chatJid); + const ownerAgentType = resolveDefaultOwnerAgentType(chatJid); + const ownerServiceId = ownerAgentType + ? getServiceIdForAgentType(ownerAgentType) + : CLAUDE_SERVICE_ID; if (roomMode === 'tribunal') { - const ownerServiceId = - hasClaude && hasCodex - ? OWNER_AGENT_TYPE === 'codex' - ? CODEX_MAIN_SERVICE_ID - : CLAUDE_SERVICE_ID - : hasCodex - ? CODEX_MAIN_SERVICE_ID - : CLAUDE_SERVICE_ID; return { chat_jid: chatJid, owner_service_id: ownerServiceId, @@ -81,33 +106,9 @@ function getDefaultLease(chatJid: string): EffectiveChannelLease { }; } - if (hasCodex) { - return { - chat_jid: chatJid, - owner_service_id: CODEX_MAIN_SERVICE_ID, - reviewer_service_id: null, - arbiter_service_id: null, - activated_at: null, - reason: null, - explicit: false, - }; - } - - if (hasClaude) { - return { - chat_jid: chatJid, - owner_service_id: CLAUDE_SERVICE_ID, - reviewer_service_id: null, - arbiter_service_id: null, - activated_at: null, - reason: null, - explicit: false, - }; - } - return { chat_jid: chatJid, - owner_service_id: CLAUDE_SERVICE_ID, + owner_service_id: ownerServiceId, reviewer_service_id: null, arbiter_service_id: null, activated_at: null,