diff --git a/src/agent-runner-environment.test.ts b/src/agent-runner-environment.test.ts index a141ff1..ff1200f 100644 --- a/src/agent-runner-environment.test.ts +++ b/src/agent-runner-environment.test.ts @@ -392,10 +392,13 @@ describe('prepareContainerSessionEnvironment codex compatibility', () => { role: 'reviewer', }); - const claudeMd = fs.readFileSync(path.join(sessionDir, 'CLAUDE.md'), 'utf-8'); - expect(fs.readFileSync(path.join(sessionDir, '.codex', 'AGENTS.md'), 'utf-8')).toBe( - claudeMd, + const claudeMd = fs.readFileSync( + path.join(sessionDir, 'CLAUDE.md'), + 'utf-8', ); + expect( + fs.readFileSync(path.join(sessionDir, '.codex', 'AGENTS.md'), 'utf-8'), + ).toBe(claudeMd); expect( fs.readFileSync(path.join(sessionDir, '.codex', 'auth.json'), 'utf-8'), ).toContain('"auth_mode":"chatgpt"'); diff --git a/src/agent-runner-environment.ts b/src/agent-runner-environment.ts index edb311c..2af7575 100644 --- a/src/agent-runner-environment.ts +++ b/src/agent-runner-environment.ts @@ -25,7 +25,10 @@ import { readPairedRoomPrompt, readPlatformPrompt, } from './platform-prompts.js'; -import { getEffectiveChannelLease, hasReviewerLease } from './service-routing.js'; +import { + getEffectiveChannelLease, + hasReviewerLease, +} from './service-routing.js'; import type { AgentType, RegisteredGroup } from './types.js'; // writeCodexApiKeyAuth removed — Codex uses OAuth only. @@ -330,10 +333,7 @@ EJCLAW_GROUP_FOLDER = ${JSON.stringify(args.group.folder)} EJCLAW_IS_MAIN = ${JSON.stringify(args.isMain ? '1' : '0')} EJCLAW_AGENT_TYPE = ${JSON.stringify(args.env.EJCLAW_AGENT_TYPE)} `; - fs.writeFileSync( - sessionConfigPath, - toml.trimEnd() + '\n' + mcpSection, - ); + fs.writeFileSync(sessionConfigPath, toml.trimEnd() + '\n' + mcpSection); } delete args.env.ANTHROPIC_API_KEY; @@ -404,7 +404,8 @@ export function prepareGroupEnvironment( // Canonical lease state now exposes owner failover directly, so prefer the // explicit flag over the older CODEX_REVIEW_SERVICE_ID shadow heuristic. const useCodexReviewFailoverPromptPack = - isReviewService(SERVICE_ID) && effectiveLease.owner_failover_active === true; + isReviewService(SERVICE_ID) && + effectiveLease.owner_failover_active === true; const ownerCommonPlatformPrompt = readOptionalPromptFile( projectRoot, diff --git a/src/arbiter-service-shadow.test.ts b/src/arbiter-service-shadow.test.ts index f408419..0d8036e 100644 --- a/src/arbiter-service-shadow.test.ts +++ b/src/arbiter-service-shadow.test.ts @@ -22,69 +22,64 @@ import { const customArbiterEnabled = ARBITER_AGENT_TYPE === 'codex' && ARBITER_SERVICE_ID != null; -describe.skipIf(!customArbiterEnabled)( - 'custom arbiter service shadow', - () => { - beforeEach(() => { - _initTestDatabase(); - clearGlobalFailover(); - refreshChannelOwnerCache(true); +describe.skipIf(!customArbiterEnabled)('custom arbiter service shadow', () => { + beforeEach(() => { + _initTestDatabase(); + clearGlobalFailover(); + refreshChannelOwnerCache(true); + }); + + it('maps codex arbiter shadow to the configured arbiter service id', () => { + expect(resolveRoleServiceShadow('arbiter', 'codex')).toBe( + ARBITER_SERVICE_ID, + ); + }); + + it('preserves the configured arbiter service id in explicit channel leases', () => { + setChannelOwnerLease({ + chat_jid: 'dc:custom-arbiter-lease', + owner_agent_type: 'codex', + reviewer_agent_type: 'claude-code', + arbiter_agent_type: 'codex', + }); + refreshChannelOwnerCache(true); + + expect(getChannelOwnerLease('dc:custom-arbiter-lease')).toMatchObject({ + owner_service_id: CODEX_MAIN_SERVICE_ID, + reviewer_service_id: CLAUDE_SERVICE_ID, + arbiter_service_id: ARBITER_SERVICE_ID, + owner_agent_type: 'codex', + reviewer_agent_type: 'claude-code', + arbiter_agent_type: 'codex', + }); + expect(getEffectiveChannelLease('dc:custom-arbiter-lease')).toMatchObject({ + owner_service_id: CODEX_MAIN_SERVICE_ID, + reviewer_service_id: CLAUDE_SERVICE_ID, + arbiter_service_id: ARBITER_SERVICE_ID, + owner_agent_type: 'codex', + reviewer_agent_type: 'claude-code', + arbiter_agent_type: 'codex', + }); + }); + + it('uses the configured arbiter service id for derived arbiter handoffs', () => { + const handoff = createServiceHandoff({ + chat_jid: 'dc:custom-arbiter-handoff', + group_folder: 'custom-arbiter-handoff', + source_role: 'owner', + source_agent_type: 'codex', + target_role: 'arbiter', + target_agent_type: 'codex', + prompt: 'arbiter please decide', + intended_role: 'arbiter', }); - it('maps codex arbiter shadow to the configured arbiter service id', () => { - expect(resolveRoleServiceShadow('arbiter', 'codex')).toBe( - ARBITER_SERVICE_ID, - ); + expect(handoff).toMatchObject({ + source_service_id: CODEX_MAIN_SERVICE_ID, + target_service_id: ARBITER_SERVICE_ID, + source_role: 'owner', + target_role: 'arbiter', + target_agent_type: 'codex', }); - - it('preserves the configured arbiter service id in explicit channel leases', () => { - setChannelOwnerLease({ - chat_jid: 'dc:custom-arbiter-lease', - owner_agent_type: 'codex', - reviewer_agent_type: 'claude-code', - arbiter_agent_type: 'codex', - }); - refreshChannelOwnerCache(true); - - expect(getChannelOwnerLease('dc:custom-arbiter-lease')).toMatchObject({ - owner_service_id: CODEX_MAIN_SERVICE_ID, - reviewer_service_id: CLAUDE_SERVICE_ID, - arbiter_service_id: ARBITER_SERVICE_ID, - owner_agent_type: 'codex', - reviewer_agent_type: 'claude-code', - arbiter_agent_type: 'codex', - }); - expect(getEffectiveChannelLease('dc:custom-arbiter-lease')).toMatchObject( - { - owner_service_id: CODEX_MAIN_SERVICE_ID, - reviewer_service_id: CLAUDE_SERVICE_ID, - arbiter_service_id: ARBITER_SERVICE_ID, - owner_agent_type: 'codex', - reviewer_agent_type: 'claude-code', - arbiter_agent_type: 'codex', - }, - ); - }); - - it('uses the configured arbiter service id for derived arbiter handoffs', () => { - const handoff = createServiceHandoff({ - chat_jid: 'dc:custom-arbiter-handoff', - group_folder: 'custom-arbiter-handoff', - source_role: 'owner', - source_agent_type: 'codex', - target_role: 'arbiter', - target_agent_type: 'codex', - prompt: 'arbiter please decide', - intended_role: 'arbiter', - }); - - expect(handoff).toMatchObject({ - source_service_id: CODEX_MAIN_SERVICE_ID, - target_service_id: ARBITER_SERVICE_ID, - source_role: 'owner', - target_role: 'arbiter', - target_agent_type: 'codex', - }); - }); - }, -); + }); +}); diff --git a/src/channels/discord.ts b/src/channels/discord.ts index d14fdf3..0856333 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -794,9 +794,7 @@ registerChannel(DISCORD_OWNER_CHANNEL, (opts: ChannelOpts) => { 'owner', ); if (!token) { - logger.warn( - 'Discord: DISCORD_OWNER_BOT_TOKEN not set', - ); + logger.warn('Discord: DISCORD_OWNER_BOT_TOKEN not set'); return null; } return new DiscordChannel(token, opts, undefined, DISCORD_OWNER_CHANNEL); diff --git a/src/db.test.ts b/src/db.test.ts index fbdcff3..17200e4 100644 --- a/src/db.test.ts +++ b/src/db.test.ts @@ -396,7 +396,9 @@ describe('session accessors', () => { const migratedDb = new Database(dbPath, { readonly: true }); expect( migratedDb - .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'service_sessions'`) + .prepare( + `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'service_sessions'`, + ) .get(), ).toBeUndefined(); migratedDb.close(); @@ -2354,29 +2356,39 @@ describe('service handoff completion', () => { const migratedDb = new Database(dbPath, { readonly: true }); expect( - (migratedDb - .prepare(`PRAGMA table_info(work_items)`) - .all() as Array<{ name: string }>).map((row) => row.name), + ( + migratedDb.prepare(`PRAGMA table_info(work_items)`).all() as Array<{ + name: string; + }> + ).map((row) => row.name), ).not.toContain('service_id'); expect( - (migratedDb - .prepare(`PRAGMA table_info(channel_owner)`) - .all() as Array<{ name: string }>).map((row) => row.name), + ( + migratedDb.prepare(`PRAGMA table_info(channel_owner)`).all() as Array<{ + name: string; + }> + ).map((row) => row.name), ).not.toContain('owner_service_id'); expect( - (migratedDb - .prepare(`PRAGMA table_info(paired_tasks)`) - .all() as Array<{ name: string }>).map((row) => row.name), + ( + migratedDb.prepare(`PRAGMA table_info(paired_tasks)`).all() as Array<{ + name: string; + }> + ).map((row) => row.name), ).not.toContain('owner_service_id'); expect( - (migratedDb - .prepare(`PRAGMA table_info(service_handoffs)`) - .all() as Array<{ name: string }>).map((row) => row.name), + ( + migratedDb + .prepare(`PRAGMA table_info(service_handoffs)`) + .all() as Array<{ name: string }> + ).map((row) => row.name), ).not.toContain('source_service_id'); expect( - (migratedDb - .prepare(`PRAGMA table_info(service_handoffs)`) - .all() as Array<{ name: string }>).map((row) => row.name), + ( + migratedDb + .prepare(`PRAGMA table_info(service_handoffs)`) + .all() as Array<{ name: string }> + ).map((row) => row.name), ).toContain('source_agent_type'); migratedDb.close(); }); @@ -2493,8 +2505,12 @@ describe('memories', () => { }); expect(recalled).toHaveLength(300); - expect(recalled.some((memory) => memory.content === 'memory-0')).toBe(false); - expect(recalled.some((memory) => memory.content === 'memory-304')).toBe(true); + expect(recalled.some((memory) => memory.content === 'memory-0')).toBe( + false, + ); + expect(recalled.some((memory) => memory.content === 'memory-304')).toBe( + true, + ); }); it('archives stale compact memories before recall using last_used_at TTL', () => { @@ -2526,9 +2542,13 @@ describe('memories', () => { limit: 10, }); - expect(recalled.some((memory) => memory.content === '오래된 compact memory')).toBe(false); expect( - recalled.some((memory) => memory.content === '최근에 다시 쓰인 compact memory'), + recalled.some((memory) => memory.content === '오래된 compact memory'), + ).toBe(false); + expect( + recalled.some( + (memory) => memory.content === '최근에 다시 쓰인 compact memory', + ), ).toBe(true); }); diff --git a/src/db.ts b/src/db.ts index 0777550..a6d4b8a 100644 --- a/src/db.ts +++ b/src/db.ts @@ -179,7 +179,10 @@ export interface ServiceHandoff { last_error: string | null; } -interface StoredWorkItemRow extends Omit { +interface StoredWorkItemRow extends Omit< + WorkItem, + 'service_id' | 'agent_type' +> { agent_type: string; } @@ -198,11 +201,13 @@ interface LegacyChannelOwnerLeaseServiceRow extends StoredChannelOwnerLeaseRow { arbiter_service_id?: string | null; } -interface StoredServiceHandoffRow - extends Omit< - ServiceHandoff, - 'source_service_id' | 'target_service_id' | 'source_agent_type' | 'target_agent_type' - > { +interface StoredServiceHandoffRow extends Omit< + ServiceHandoff, + | 'source_service_id' + | 'target_service_id' + | 'source_agent_type' + | 'target_agent_type' +> { source_agent_type?: string | null; target_agent_type: string; } @@ -212,15 +217,14 @@ interface LegacyServiceHandoffServiceRow extends StoredServiceHandoffRow { target_service_id?: string | null; } -interface StoredPairedTaskRow - extends Omit< - PairedTask, - | 'owner_service_id' - | 'reviewer_service_id' - | 'owner_agent_type' - | 'reviewer_agent_type' - | 'arbiter_agent_type' - > { +interface StoredPairedTaskRow extends Omit< + PairedTask, + | 'owner_service_id' + | 'reviewer_service_id' + | 'owner_agent_type' + | 'reviewer_agent_type' + | 'arbiter_agent_type' +> { owner_agent_type?: string | null; reviewer_agent_type?: string | null; arbiter_agent_type?: string | null; @@ -286,20 +290,19 @@ function hydrateChannelOwnerLeaseRow( const reviewerAgentType = row.reviewer_agent_type == null ? null - : normalizeStoredAgentType(row.reviewer_agent_type) ?? - resolveStableReviewerAgentType(ownerAgentType, null); + : (normalizeStoredAgentType(row.reviewer_agent_type) ?? + resolveStableReviewerAgentType(ownerAgentType, null)); const arbiterAgentType = row.arbiter_agent_type == null ? null - : normalizeStoredAgentType(row.arbiter_agent_type) ?? + : (normalizeStoredAgentType(row.arbiter_agent_type) ?? ARBITER_AGENT_TYPE ?? - null; + null); return { chat_jid: row.chat_jid, owner_service_id: - resolveRoleServiceShadow('owner', ownerAgentType) ?? - CLAUDE_SERVICE_ID, + resolveRoleServiceShadow('owner', ownerAgentType) ?? CLAUDE_SERVICE_ID, reviewer_service_id: reviewerAgentType == null ? null @@ -323,12 +326,15 @@ function hydratePairedTaskRow(row: StoredPairedTaskRow): PairedTask { row.reviewer_agent_type ?? null, ); const arbiterAgentType = - normalizeStoredAgentType(row.arbiter_agent_type) ?? ARBITER_AGENT_TYPE ?? null; + normalizeStoredAgentType(row.arbiter_agent_type) ?? + ARBITER_AGENT_TYPE ?? + null; return { ...row, owner_service_id: - resolveRoleServiceShadow('owner', ownerAgentType) ?? CODEX_MAIN_SERVICE_ID, + resolveRoleServiceShadow('owner', ownerAgentType) ?? + CODEX_MAIN_SERVICE_ID, reviewer_service_id: resolveRoleServiceShadow('reviewer', reviewerAgentType) ?? CODEX_REVIEW_SERVICE_ID, @@ -338,7 +344,9 @@ function hydratePairedTaskRow(row: StoredPairedTaskRow): PairedTask { }; } -function hydrateServiceHandoffRow(row: StoredServiceHandoffRow): ServiceHandoff { +function hydrateServiceHandoffRow( + row: StoredServiceHandoffRow, +): ServiceHandoff { const sourceAgentType = normalizeStoredAgentType(row.source_agent_type) ?? (row.source_role @@ -365,22 +373,22 @@ function hydrateServiceHandoffRow(row: StoredServiceHandoffRow): ServiceHandoff target_agent_type: targetAgentType, source_service_id: row.source_role != null - ? resolveRoleServiceShadow(row.source_role, sourceAgentType) ?? - SERVICE_SESSION_SCOPE + ? (resolveRoleServiceShadow(row.source_role, sourceAgentType) ?? + SERVICE_SESSION_SCOPE) : SERVICE_SESSION_SCOPE, target_service_id: row.target_role != null - ? resolveRoleServiceShadow(row.target_role, targetAgentType) ?? - SERVICE_SESSION_SCOPE + ? (resolveRoleServiceShadow(row.target_role, targetAgentType) ?? + SERVICE_SESSION_SCOPE) : SERVICE_SESSION_SCOPE, }; } function getTableColumns(database: Database, tableName: string): string[] { return ( - database - .prepare(`PRAGMA table_info(${tableName})`) - .all() as Array<{ name: string }> + database.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ + name: string; + }> ).map((column) => column.name); } @@ -783,7 +791,9 @@ function createSchema(database: Database): void { } try { - database.exec(`ALTER TABLE service_handoffs ADD COLUMN source_agent_type TEXT`); + database.exec( + `ALTER TABLE service_handoffs ADD COLUMN source_agent_type TEXT`, + ); } catch { /* column already exists */ } @@ -795,13 +805,17 @@ function createSchema(database: Database): void { } try { - database.exec(`ALTER TABLE paired_tasks ADD COLUMN reviewer_agent_type TEXT`); + database.exec( + `ALTER TABLE paired_tasks ADD COLUMN reviewer_agent_type TEXT`, + ); } catch { /* column already exists */ } try { - database.exec(`ALTER TABLE paired_tasks ADD COLUMN arbiter_agent_type TEXT`); + database.exec( + `ALTER TABLE paired_tasks ADD COLUMN arbiter_agent_type TEXT`, + ); } catch { /* column already exists */ } @@ -1362,9 +1376,7 @@ function normalizeMemoryKeywords(keywords?: string[]): string[] { if (!Array.isArray(keywords)) return []; return [ ...new Set( - keywords - .map((keyword) => keyword.trim().toLowerCase()) - .filter(Boolean), + keywords.map((keyword) => keyword.trim().toLowerCase()).filter(Boolean), ), ]; } @@ -1453,7 +1465,9 @@ function queryFtsRowOrder(query: RecallMemoryQuery): Map { } export function touchMemories(ids: number[]): void { - const uniqueIds = [...new Set(ids.filter((id) => Number.isInteger(id) && id > 0))]; + const uniqueIds = [ + ...new Set(ids.filter((id) => Number.isInteger(id) && id > 0)), + ]; if (uniqueIds.length === 0) return; const now = new Date().toISOString(); const stmt = db.prepare( @@ -1573,7 +1587,9 @@ export function rememberMemory(input: { input.sourceRef ?? null, createdAt, ); - const row = db.prepare('SELECT last_insert_rowid() AS id').get() as { id: number }; + const row = db.prepare('SELECT last_insert_rowid() AS id').get() as { + id: number; + }; expireStaleMemories({ scopeKind: input.scopeKind, scopeKey: input.scopeKey, @@ -1598,8 +1614,9 @@ export function recallMemories(query: RecallMemoryQuery): MemoryRecord[] { const scored = rows .map((row, index) => { const keywords = parseMemoryKeywords(row.keywords_json); - const exactMatches = keywords.filter((keyword) => exactKeywords.has(keyword)) - .length; + const exactMatches = keywords.filter((keyword) => + exactKeywords.has(keyword), + ).length; const ftsScore = ftsOrder.get(row.id) ?? 0; const recencyScore = rows.length - index; return { @@ -2016,12 +2033,9 @@ export function getOpenWorkItem( id ASC LIMIT 1`, ) - .get( - chatJid, - agentType, - preferredRole, - preferredRole, - ) as StoredWorkItemRow | undefined; + .get(chatJid, agentType, preferredRole, preferredRole) as + | StoredWorkItemRow + | undefined; return row ? hydrateWorkItem(row) : undefined; } @@ -2066,9 +2080,9 @@ export function createProducedWorkItem(input: { db.prepare('SELECT last_insert_rowid() as id').get() as { id: number } ).id; return hydrateWorkItem( - db.prepare('SELECT * FROM work_items WHERE id = ?').get( - lastId, - ) as StoredWorkItemRow, + db + .prepare('SELECT * FROM work_items WHERE id = ?') + .get(lastId) as StoredWorkItemRow, ); } @@ -2474,9 +2488,7 @@ export function getSession( .prepare( 'SELECT session_id FROM sessions WHERE group_folder = ? AND agent_type = ?', ) - .get(groupFolder, agentType) as - | { session_id: string } - | undefined; + .get(groupFolder, agentType) as { session_id: string } | undefined; return row?.session_id; } @@ -2613,8 +2625,7 @@ function buildRegisteredGroupFromStoredSettings( name: stored.name || stored.chatJid, folder: stored.folder, trigger: stored.trigger || `@${ASSISTANT_NAME}`, - added_at: - capabilityMetadata?.added_at || new Date(0).toISOString(), + added_at: capabilityMetadata?.added_at || new Date(0).toISOString(), agentConfig: capabilityMetadata?.agentConfig, requiresTrigger: stored.requiresTrigger, isMain: stored.isMain ? true : undefined, @@ -2692,12 +2703,16 @@ export function assignRoom( const roomMode = input.roomMode || existing?.roomMode || 'single'; const ownerAgentType = input.ownerAgentType || existing?.ownerAgentType || OWNER_AGENT_TYPE; - const folder = resolveAssignedRoomFolder(db, chatJid, input.name, input.folder); + const folder = resolveAssignedRoomFolder( + db, + chatJid, + input.name, + input.folder, + ); const snapshot: RoomRegistrationSnapshot = { name: input.name, folder, - triggerPattern: - input.trigger || existing?.trigger || `@${ASSISTANT_NAME}`, + triggerPattern: input.trigger || existing?.trigger || `@${ASSISTANT_NAME}`, requiresTrigger: input.requiresTrigger ?? existing?.requiresTrigger ?? true, isMain: input.isMain ?? existing?.isMain ?? false, ownerAgentType, @@ -2911,7 +2926,9 @@ function getLegacyRegisteredGroup( 'SELECT * FROM registered_groups WHERE jid = ? AND agent_type = ?', ) .get(jid, agentType) - : database.prepare('SELECT * FROM registered_groups WHERE jid = ?').get(jid) + : database + .prepare('SELECT * FROM registered_groups WHERE jid = ?') + .get(jid) ) as RegisteredGroupDatabaseRow | undefined; return parseRegisteredGroupRow(row); } @@ -2949,7 +2966,9 @@ function getRegisteredGroupCapabilityMetadata( }; } -function getStoredRoomRowsFromDatabase(database: Database): StoredRoomSettings[] { +function getStoredRoomRowsFromDatabase( + database: Database, +): StoredRoomSettings[] { return database .prepare( `SELECT chat_jid @@ -2958,7 +2977,10 @@ function getStoredRoomRowsFromDatabase(database: Database): StoredRoomSettings[] ) .all() .map((row) => - getStoredRoomSettingsRowFromDatabase(database, (row as { chat_jid: string }).chat_jid), + getStoredRoomSettingsRowFromDatabase( + database, + (row as { chat_jid: string }).chat_jid, + ), ) .filter((row): row is StoredRoomSettings => Boolean(row)); } @@ -2995,7 +3017,12 @@ function collectReservedFolders( FROM registered_groups WHERE ? IS NULL OR jid != ?`, ) - .all(exceptChatJid ?? null, exceptChatJid ?? null, exceptChatJid ?? null, exceptChatJid ?? null) as Array<{ + .all( + exceptChatJid ?? null, + exceptChatJid ?? null, + exceptChatJid ?? null, + exceptChatJid ?? null, + ) as Array<{ folder: string | null; }>; @@ -3042,7 +3069,9 @@ function resolveAssignedRoomFolder( const reserved = collectReservedFolders(database, chatJid); if (explicitFolder) { if (!isValidGroupFolder(explicitFolder)) { - throw new Error(`Invalid group folder "${explicitFolder}" for JID ${chatJid}`); + throw new Error( + `Invalid group folder "${explicitFolder}" for JID ${chatJid}`, + ); } if (reserved.has(explicitFolder)) { throw new Error(`Group folder "${explicitFolder}" is already assigned`); @@ -3050,7 +3079,10 @@ function resolveAssignedRoomFolder( return explicitFolder; } - const existingFolder = getStoredRoomSettingsRowFromDatabase(database, chatJid)?.folder; + const existingFolder = getStoredRoomSettingsRowFromDatabase( + database, + chatJid, + )?.folder; if (existingFolder) return existingFolder; return buildGeneratedRoomFolder(database, chatJid, name); @@ -3089,7 +3121,10 @@ function materializeRegisteredGroupsForRoom( added_at: string; agent_config: string | null; }>; - const existingByType = new Map(); + const existingByType = new Map< + AgentType, + { added_at: string; agent_config: string | null } + >(); for (const row of existingRows) { const agentType = normalizeStoredAgentType(row.agent_type); if (agentType) { @@ -3101,8 +3136,10 @@ function materializeRegisteredGroupsForRoom( const existing = existingByType.get(agentType); const agentConfig = agentType === ownerAgentType - ? ownerAgentConfig ?? - (existing?.agent_config ? JSON.parse(existing.agent_config) : undefined) + ? (ownerAgentConfig ?? + (existing?.agent_config + ? JSON.parse(existing.agent_config) + : undefined)) : existing?.agent_config ? JSON.parse(existing.agent_config) : undefined; @@ -3262,9 +3299,8 @@ function collectRoomRegistrationSnapshot( } const agentTypes = collectRegisteredAgentTypes(database, jid); - const inferredOwnerAgentType = inferOwnerAgentTypeFromRegisteredAgentTypes( - agentTypes, - ); + const inferredOwnerAgentType = + inferOwnerAgentTypeFromRegisteredAgentTypes(agentTypes); const preferExplicitTrigger = existingStored?.modeSource === 'explicit' && existingStored.trigger; const preferExplicitOwner = @@ -3280,7 +3316,8 @@ function collectRoomRegistrationSnapshot( : undefined; const inferredOwnerRow = rows.find( - (row) => normalizeStoredAgentType(row.agent_type) === inferredOwnerAgentType, + (row) => + normalizeStoredAgentType(row.agent_type) === inferredOwnerAgentType, ) ?? rows[0]; const ownerAgentType = preferredOwnerAgentType ? preferredOwnerRow @@ -3292,12 +3329,11 @@ function collectRoomRegistrationSnapshot( return { name: first.name, folder: first.folder, - triggerPattern: - preferExplicitTrigger - ? existingStored.trigger! - : preferredOwnerRow != null - ? preferredOwnerRow.trigger_pattern - : ownerRow.trigger_pattern, + triggerPattern: preferExplicitTrigger + ? existingStored.trigger! + : preferredOwnerRow != null + ? preferredOwnerRow.trigger_pattern + : ownerRow.trigger_pattern, requiresTrigger: (first.requires_trigger ?? 1) === 1, isMain: (first.is_main ?? 0) === 1, ownerAgentType, @@ -3376,14 +3412,20 @@ function updateStoredRoomMetadata( function syncStoredRoomRegistrationSnapshotForJid(chatJid: string): void { const existingSettings = getStoredRoomSettingsRowFromDatabase(db, chatJid); - const snapshot = collectRoomRegistrationSnapshot(db, chatJid, existingSettings); + const snapshot = collectRoomRegistrationSnapshot( + db, + chatJid, + existingSettings, + ); if (!snapshot) return; if (!existingSettings) { insertStoredRoomSettings( db, chatJid, - inferRoomModeFromRegisteredAgentTypes(getRegisteredAgentTypesForJid(chatJid)), + inferRoomModeFromRegisteredAgentTypes( + getRegisteredAgentTypesForJid(chatJid), + ), 'inferred', snapshot, ); @@ -3521,9 +3563,14 @@ function collectRegisteredAgentTypesForFolder( function resolveStablePairedTaskOwnerAgentType( database: Database, - task: Pick, + task: Pick< + StoredPairedTaskRow, + 'chat_jid' | 'group_folder' | 'owner_agent_type' + >, ): AgentType | undefined { - const persistedOwnerAgentType = normalizeStoredAgentType(task.owner_agent_type); + const persistedOwnerAgentType = normalizeStoredAgentType( + task.owner_agent_type, + ); if (persistedOwnerAgentType) { return persistedOwnerAgentType; } @@ -3729,13 +3776,13 @@ function backfillChannelOwnerRoleMetadata(database: Database): void { const reviewerServiceId = row.reviewer_service_id == null ? null - : resolveRoleServiceShadow('reviewer', reviewerAgentType) ?? - row.reviewer_service_id; + : (resolveRoleServiceShadow('reviewer', reviewerAgentType) ?? + row.reviewer_service_id); const arbiterServiceId = row.arbiter_service_id == null ? null - : resolveRoleServiceShadow('arbiter', arbiterAgentType) ?? - row.arbiter_service_id; + : (resolveRoleServiceShadow('arbiter', arbiterAgentType) ?? + row.arbiter_service_id); if ( ownerServiceId === row.owner_service_id && @@ -3911,13 +3958,13 @@ function backfillServiceHandoffServiceShadows(database: Database): void { const normalizedSourceServiceId = row.source_role != null - ? resolveRoleServiceShadow(row.source_role, sourceAgentType) ?? - row.source_service_id + ? (resolveRoleServiceShadow(row.source_role, sourceAgentType) ?? + row.source_service_id) : row.source_service_id; const normalizedTargetServiceId = row.target_role != null - ? resolveRoleServiceShadow(row.target_role, targetAgentType) ?? - row.target_service_id + ? (resolveRoleServiceShadow(row.target_role, targetAgentType) ?? + row.target_service_id) : row.target_service_id; if ( @@ -4624,16 +4671,16 @@ export function setChannelOwnerLease(input: { const reviewerAgentType = input.reviewer_service_id == null && input.reviewer_agent_type == null ? null - : normalizeStoredAgentType(input.reviewer_agent_type) ?? + : (normalizeStoredAgentType(input.reviewer_agent_type) ?? inferAgentTypeFromServiceShadow(input.reviewer_service_id ?? null) ?? - resolveStableReviewerAgentType(ownerAgentType, null); + resolveStableReviewerAgentType(ownerAgentType, null)); const arbiterAgentType = input.arbiter_service_id == null && input.arbiter_agent_type == null ? null - : normalizeStoredAgentType(input.arbiter_agent_type) ?? + : (normalizeStoredAgentType(input.arbiter_agent_type) ?? inferAgentTypeFromServiceShadow(input.arbiter_service_id ?? null) ?? ARBITER_AGENT_TYPE ?? - null; + null); db.prepare( `INSERT OR REPLACE INTO channel_owner ( @@ -4719,9 +4766,9 @@ export function createServiceHandoff(input: { db.prepare('SELECT last_insert_rowid() as id').get() as { id: number } ).id; return hydrateServiceHandoffRow( - db.prepare('SELECT * FROM service_handoffs WHERE id = ?').get( - lastId, - ) as StoredServiceHandoffRow, + db + .prepare('SELECT * FROM service_handoffs WHERE id = ?') + .get(lastId) as StoredServiceHandoffRow, ); } diff --git a/src/ipc-auth.test.ts b/src/ipc-auth.test.ts index d0eeecc..b839a53 100644 --- a/src/ipc-auth.test.ts +++ b/src/ipc-auth.test.ts @@ -894,10 +894,9 @@ describe('assign_room success', () => { modeSource: 'explicit', name: 'Legacy Tribunal Renamed', }); - expect(getRegisteredAgentTypesForJid('legacy-tribunal@g.us').sort()).toEqual([ - 'claude-code', - 'codex', - ]); + expect( + getRegisteredAgentTypesForJid('legacy-tribunal@g.us').sort(), + ).toEqual(['claude-code', 'codex']); }); it('main group can assign a tribunal room and materialize capability rows', async () => { diff --git a/src/ipc.ts b/src/ipc.ts index 06528f8..3c9a981 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -701,7 +701,10 @@ export async function processTaskIpc( break; } - if (Array.isArray(data.keywords) && !data.keywords.every((v) => typeof v === 'string')) { + if ( + Array.isArray(data.keywords) && + !data.keywords.every((v) => typeof v === 'string') + ) { logger.warn( { sourceGroup, keywords: data.keywords }, 'Invalid persist_memory request - keywords must be strings', @@ -717,10 +720,13 @@ export async function processTaskIpc( memoryKind: typeof data.memory_kind === 'string' ? data.memory_kind : null, sourceKind: - (data.source_kind as 'compact' | 'explicit' | 'import' | 'system' | undefined) ?? - 'compact', - sourceRef: - typeof data.source_ref === 'string' ? data.source_ref : null, + (data.source_kind as + | 'compact' + | 'explicit' + | 'import' + | 'system' + | undefined) ?? 'compact', + sourceRef: typeof data.source_ref === 'string' ? data.source_ref : null, }); logger.info( { diff --git a/src/message-agent-executor.ts b/src/message-agent-executor.ts index 9f54611..fb45f32 100644 --- a/src/message-agent-executor.ts +++ b/src/message-agent-executor.ts @@ -115,8 +115,8 @@ export async function runAgentForGroup( const inferredRole = resolveActiveRole(pairedTask?.status); const canHonorForcedRole = Boolean( args.forcedRole === 'owner' || - (args.forcedRole === 'reviewer' && currentLease.reviewer_agent_type) || - (args.forcedRole === 'arbiter' && currentLease.arbiter_agent_type), + (args.forcedRole === 'reviewer' && currentLease.reviewer_agent_type) || + (args.forcedRole === 'arbiter' && currentLease.arbiter_agent_type), ); const activeRole = canHonorForcedRole ? args.forcedRole! : inferredRole; const effectiveServiceId = resolveLeaseServiceId(currentLease, activeRole); diff --git a/src/message-runtime.ts b/src/message-runtime.ts index a049522..1e36767 100644 --- a/src/message-runtime.ts +++ b/src/message-runtime.ts @@ -20,9 +20,7 @@ import { type ServiceHandoff, type WorkItem, } from './db.js'; -import { - isSessionCommandSenderAllowed, -} from './config.js'; +import { isSessionCommandSenderAllowed } from './config.js'; import { GroupQueue, GroupRunContext } from './group-queue.js'; import { findChannel, @@ -61,9 +59,7 @@ import { } from './types.js'; import { createScopedLogger, logger } from './logger.js'; import { resolveGroupIpcPath } from './group-folder.js'; -import { - hasReviewerLease, -} from './service-routing.js'; +import { hasReviewerLease } from './service-routing.js'; /** * Check if a message is a duplicate of the last bot final message in a paired room. diff --git a/src/paired-workspace-manager.test.ts b/src/paired-workspace-manager.test.ts index d54d333..fd8e687 100644 --- a/src/paired-workspace-manager.test.ts +++ b/src/paired-workspace-manager.test.ts @@ -88,7 +88,12 @@ function ownerBranchName(groupFolder: string): string { } function ownerWorkspacePath(groupFolder: string): string { - return path.join(process.env.EJCLAW_DATA_DIR!, 'workspaces', groupFolder, 'owner'); + return path.join( + process.env.EJCLAW_DATA_DIR!, + 'workspaces', + groupFolder, + 'owner', + ); } describe('paired workspace manager', () => { @@ -739,14 +744,18 @@ describe('paired workspace manager', () => { groupFolder: 'head-base-room', }); - const ownerWorkspace = - manager.provisionOwnerWorkspaceForPairedTask('paired-task-head-base'); + const ownerWorkspace = manager.provisionOwnerWorkspaceForPairedTask( + 'paired-task-head-base', + ); expect( runGit(['branch', '--show-current'], ownerWorkspace.workspace_dir), ).toBe(ownerBranchName('head-base-room')); expect( - fs.readFileSync(path.join(ownerWorkspace.workspace_dir, 'README.md'), 'utf-8'), + fs.readFileSync( + path.join(ownerWorkspace.workspace_dir, 'README.md'), + 'utf-8', + ), ).toBe('feature base\n'); expect(runGit(['rev-parse', 'HEAD'], ownerWorkspace.workspace_dir)).toBe( runGit(['rev-parse', 'HEAD'], canonicalDir), @@ -781,7 +790,10 @@ describe('paired workspace manager', () => { runGit(['branch', '--show-current'], secondProvision.workspace_dir), ).toBe(ownerBranchName('tree-source-room')); expect( - fs.readFileSync(path.join(secondProvision.workspace_dir, 'README.md'), 'utf-8'), + fs.readFileSync( + path.join(secondProvision.workspace_dir, 'README.md'), + 'utf-8', + ), ).toBe('tree source\n'); expect(runGit(['rev-parse', 'HEAD'], secondProvision.workspace_dir)).toBe( runGit(['rev-parse', 'HEAD'], canonicalDir), @@ -814,9 +826,9 @@ describe('paired workspace manager', () => { expect( runGit(['branch', '--show-current'], reprovisioned.workspace_dir), ).toBe(ownerBranchName('repair-room')); - expect( - runGit(['worktree', 'list', '--porcelain'], canonicalDir), - ).toContain(reprovisioned.workspace_dir); + expect(runGit(['worktree', 'list', '--porcelain'], canonicalDir)).toContain( + reprovisioned.workspace_dir, + ); }); it('lazy-migrates a detached dirty owner workspace to a new channel branch', async () => { @@ -844,17 +856,23 @@ describe('paired workspace manager', () => { runGit(['branch', '--show-current'], ownerWorkspace.workspace_dir), ).toBe(ownerBranchName('migrate-room')); expect( - fs.readFileSync(path.join(ownerWorkspace.workspace_dir, 'README.md'), 'utf-8'), + fs.readFileSync( + path.join(ownerWorkspace.workspace_dir, 'README.md'), + 'utf-8', + ), ).toBe('detached dirty\n'); expect( - fs.readFileSync(path.join(ownerWorkspace.workspace_dir, 'NEW_FILE.txt'), 'utf-8'), + fs.readFileSync( + path.join(ownerWorkspace.workspace_dir, 'NEW_FILE.txt'), + 'utf-8', + ), ).toBe('new file\n'); - expect(runGit(['status', '--short'], ownerWorkspace.workspace_dir)).toContain( - 'M README.md', - ); - expect(runGit(['status', '--short'], ownerWorkspace.workspace_dir)).toContain( - '?? NEW_FILE.txt', - ); + expect( + runGit(['status', '--short'], ownerWorkspace.workspace_dir), + ).toContain('M README.md'); + expect( + runGit(['status', '--short'], ownerWorkspace.workspace_dir), + ).toContain('?? NEW_FILE.txt'); }); it('lazy-migrates a detached dirty owner workspace onto an existing matching channel branch', async () => { @@ -875,7 +893,10 @@ describe('paired workspace manager', () => { const workspaceDir = ownerWorkspacePath('migrate-existing-room'); fs.mkdirSync(path.dirname(workspaceDir), { recursive: true }); runGit(['worktree', 'add', workspaceDir, 'HEAD'], canonicalDir); - fs.writeFileSync(path.join(workspaceDir, 'README.md'), 'detached existing\n'); + fs.writeFileSync( + path.join(workspaceDir, 'README.md'), + 'detached existing\n', + ); fs.writeFileSync(path.join(workspaceDir, 'NOTES.md'), 'keep me\n'); const ownerWorkspace = manager.provisionOwnerWorkspaceForPairedTask( @@ -886,10 +907,16 @@ describe('paired workspace manager', () => { runGit(['branch', '--show-current'], ownerWorkspace.workspace_dir), ).toBe(ownerBranchName('migrate-existing-room')); expect( - fs.readFileSync(path.join(ownerWorkspace.workspace_dir, 'README.md'), 'utf-8'), + fs.readFileSync( + path.join(ownerWorkspace.workspace_dir, 'README.md'), + 'utf-8', + ), ).toBe('detached existing\n'); expect( - fs.readFileSync(path.join(ownerWorkspace.workspace_dir, 'NOTES.md'), 'utf-8'), + fs.readFileSync( + path.join(ownerWorkspace.workspace_dir, 'NOTES.md'), + 'utf-8', + ), ).toBe('keep me\n'); }); diff --git a/src/paired-workspace-manager.ts b/src/paired-workspace-manager.ts index 2a26200..cb99dcf 100644 --- a/src/paired-workspace-manager.ts +++ b/src/paired-workspace-manager.ts @@ -588,7 +588,14 @@ export function provisionOwnerWorkspaceForPairedTask( runGit(['worktree', 'add', workspaceDir, targetBranch], canonicalWorkDir); } else { runGit( - ['worktree', 'add', '-b', targetBranch, workspaceDir, canonicalHeadCommit], + [ + 'worktree', + 'add', + '-b', + targetBranch, + workspaceDir, + canonicalHeadCommit, + ], canonicalWorkDir, ); } diff --git a/src/service-routing.ts b/src/service-routing.ts index 0df7b2a..2e599c0 100644 --- a/src/service-routing.ts +++ b/src/service-routing.ts @@ -55,14 +55,14 @@ function normalizeLeaseRow( const reviewerAgentType = row.reviewer_service_id == null ? null - : row.reviewer_agent_type ?? + : (row.reviewer_agent_type ?? inferAgentTypeFromServiceShadow(row.reviewer_service_id) ?? resolveRoleAgentPlan({ paired: true, groupAgentType: ownerAgentType, configuredReviewer: REVIEWER_AGENT_TYPE, configuredArbiter: ARBITER_AGENT_TYPE, - }).reviewerAgentType; + }).reviewerAgentType); const arbiterAgentType = row.arbiter_agent_type ?? (row.arbiter_service_id @@ -79,12 +79,14 @@ function normalizeLeaseRow( resolveRoleServiceShadow('owner', ownerAgentType) ?? normalizeServiceId(row.owner_service_id), reviewer_service_id: reviewerAgentType - ? resolveRoleServiceShadow('reviewer', reviewerAgentType) ?? - normalizeServiceId(row.reviewer_service_id!) + ? (resolveRoleServiceShadow('reviewer', reviewerAgentType) ?? + normalizeServiceId(row.reviewer_service_id!)) : null, arbiter_service_id: arbiterAgentType - ? resolveRoleServiceShadow('arbiter', arbiterAgentType) ?? - (row.arbiter_service_id ? normalizeServiceId(row.arbiter_service_id) : null) + ? (resolveRoleServiceShadow('arbiter', arbiterAgentType) ?? + (row.arbiter_service_id + ? normalizeServiceId(row.arbiter_service_id) + : null)) : row.arbiter_service_id ? normalizeServiceId(row.arbiter_service_id) : isArbiterEnabled() @@ -218,19 +220,19 @@ export function resolveLeaseServiceId( ); case 'reviewer': return lease.reviewer_agent_type - ? resolveRoleServiceShadow('reviewer', lease.reviewer_agent_type) ?? + ? (resolveRoleServiceShadow('reviewer', lease.reviewer_agent_type) ?? (lease.reviewer_service_id ? normalizeServiceId(lease.reviewer_service_id) - : null) + : null)) : lease.reviewer_service_id ? normalizeServiceId(lease.reviewer_service_id) : null; case 'arbiter': return lease.arbiter_agent_type - ? resolveRoleServiceShadow('arbiter', lease.arbiter_agent_type) ?? + ? (resolveRoleServiceShadow('arbiter', lease.arbiter_agent_type) ?? (lease.arbiter_service_id ? normalizeServiceId(lease.arbiter_service_id) - : null) + : null)) : lease.arbiter_service_id ? normalizeServiceId(lease.arbiter_service_id) : null; @@ -242,7 +244,9 @@ export function isOwnerServiceForChat( serviceId: string = SERVICE_ID, ): boolean { const lease = getEffectiveChannelLease(chatJid); - return normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'owner'); + return ( + normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'owner') + ); } export function isReviewerServiceForChat( @@ -250,11 +254,16 @@ export function isReviewerServiceForChat( serviceId: string = SERVICE_ID, ): boolean { const lease = getEffectiveChannelLease(chatJid); - return normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'reviewer'); + return ( + normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'reviewer') + ); } export function hasReviewerLease(chatJid: string): boolean { - return resolveLeaseServiceId(getEffectiveChannelLease(chatJid), 'reviewer') !== null; + return ( + resolveLeaseServiceId(getEffectiveChannelLease(chatJid), 'reviewer') !== + null + ); } export function isArbiterServiceForChat( @@ -262,7 +271,9 @@ export function isArbiterServiceForChat( serviceId: string = SERVICE_ID, ): boolean { const lease = getEffectiveChannelLease(chatJid); - return normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'arbiter'); + return ( + normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'arbiter') + ); } export function shouldServiceProcessChat( diff --git a/src/sqlite-memory-store.test.ts b/src/sqlite-memory-store.test.ts index 200e8fb..255699c 100644 --- a/src/sqlite-memory-store.test.ts +++ b/src/sqlite-memory-store.test.ts @@ -35,7 +35,8 @@ describe('sqlite-memory-store helpers', () => { id: 2, scopeKind: 'room', scopeKey: 'room:ejclaw', - content: '자동 recall/compact persist를 호스트가 책임지는 방향으로 합의함.', + content: + '자동 recall/compact persist를 호스트가 책임지는 방향으로 합의함.', keywords: ['room:ejclaw'], memoryKind: null, sourceKind: 'compact', diff --git a/src/task-scheduler.test.ts b/src/task-scheduler.test.ts index 707105a..6fdeda4 100644 --- a/src/task-scheduler.test.ts +++ b/src/task-scheduler.test.ts @@ -138,11 +138,7 @@ vi.mock('./service-routing.js', () => ({ hasReviewerLease: vi.fn(() => false), })); -import { - _initTestDatabase, - createTask, - getTaskById, -} from './db.js'; +import { _initTestDatabase, createTask, getTaskById } from './db.js'; import * as codexTokenRotation from './codex-token-rotation.js'; import { TIMEZONE } from './config.js'; import * as serviceRouting from './service-routing.js'; diff --git a/src/task-scheduler.ts b/src/task-scheduler.ts index 000f356..ce13440 100644 --- a/src/task-scheduler.ts +++ b/src/task-scheduler.ts @@ -757,7 +757,11 @@ async function runGithubCiTask( if (check.terminal) { await statusTracker.update('completed'); if (check.completionMessage) { - await sendScheduledMessage(deps, task.chat_jid, check.completionMessage); + await sendScheduledMessage( + deps, + task.chat_jid, + check.completionMessage, + ); } deleteTask(task.id); completedAndDeleted = true;