Apply formatter after local memory refactor

This commit is contained in:
Eyejoker
2026-04-04 03:52:28 +09:00
parent 3dd772c229
commit 54abb5bb4e
16 changed files with 360 additions and 249 deletions

View File

@@ -392,10 +392,13 @@ describe('prepareContainerSessionEnvironment codex compatibility', () => {
role: 'reviewer', role: 'reviewer',
}); });
const claudeMd = fs.readFileSync(path.join(sessionDir, 'CLAUDE.md'), 'utf-8'); const claudeMd = fs.readFileSync(
expect(fs.readFileSync(path.join(sessionDir, '.codex', 'AGENTS.md'), 'utf-8')).toBe( path.join(sessionDir, 'CLAUDE.md'),
claudeMd, 'utf-8',
); );
expect(
fs.readFileSync(path.join(sessionDir, '.codex', 'AGENTS.md'), 'utf-8'),
).toBe(claudeMd);
expect( expect(
fs.readFileSync(path.join(sessionDir, '.codex', 'auth.json'), 'utf-8'), fs.readFileSync(path.join(sessionDir, '.codex', 'auth.json'), 'utf-8'),
).toContain('"auth_mode":"chatgpt"'); ).toContain('"auth_mode":"chatgpt"');

View File

@@ -25,7 +25,10 @@ import {
readPairedRoomPrompt, readPairedRoomPrompt,
readPlatformPrompt, readPlatformPrompt,
} from './platform-prompts.js'; } 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'; import type { AgentType, RegisteredGroup } from './types.js';
// writeCodexApiKeyAuth removed — Codex uses OAuth only. // 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_IS_MAIN = ${JSON.stringify(args.isMain ? '1' : '0')}
EJCLAW_AGENT_TYPE = ${JSON.stringify(args.env.EJCLAW_AGENT_TYPE)} EJCLAW_AGENT_TYPE = ${JSON.stringify(args.env.EJCLAW_AGENT_TYPE)}
`; `;
fs.writeFileSync( fs.writeFileSync(sessionConfigPath, toml.trimEnd() + '\n' + mcpSection);
sessionConfigPath,
toml.trimEnd() + '\n' + mcpSection,
);
} }
delete args.env.ANTHROPIC_API_KEY; delete args.env.ANTHROPIC_API_KEY;
@@ -404,7 +404,8 @@ export function prepareGroupEnvironment(
// Canonical lease state now exposes owner failover directly, so prefer the // Canonical lease state now exposes owner failover directly, so prefer the
// explicit flag over the older CODEX_REVIEW_SERVICE_ID shadow heuristic. // explicit flag over the older CODEX_REVIEW_SERVICE_ID shadow heuristic.
const useCodexReviewFailoverPromptPack = const useCodexReviewFailoverPromptPack =
isReviewService(SERVICE_ID) && effectiveLease.owner_failover_active === true; isReviewService(SERVICE_ID) &&
effectiveLease.owner_failover_active === true;
const ownerCommonPlatformPrompt = readOptionalPromptFile( const ownerCommonPlatformPrompt = readOptionalPromptFile(
projectRoot, projectRoot,

View File

@@ -22,69 +22,64 @@ import {
const customArbiterEnabled = const customArbiterEnabled =
ARBITER_AGENT_TYPE === 'codex' && ARBITER_SERVICE_ID != null; ARBITER_AGENT_TYPE === 'codex' && ARBITER_SERVICE_ID != null;
describe.skipIf(!customArbiterEnabled)( describe.skipIf(!customArbiterEnabled)('custom arbiter service shadow', () => {
'custom arbiter service shadow', beforeEach(() => {
() => { _initTestDatabase();
beforeEach(() => { clearGlobalFailover();
_initTestDatabase(); refreshChannelOwnerCache(true);
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(handoff).toMatchObject({
expect(resolveRoleServiceShadow('arbiter', 'codex')).toBe( source_service_id: CODEX_MAIN_SERVICE_ID,
ARBITER_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',
});
});
},
);

View File

@@ -794,9 +794,7 @@ registerChannel(DISCORD_OWNER_CHANNEL, (opts: ChannelOpts) => {
'owner', 'owner',
); );
if (!token) { if (!token) {
logger.warn( logger.warn('Discord: DISCORD_OWNER_BOT_TOKEN not set');
'Discord: DISCORD_OWNER_BOT_TOKEN not set',
);
return null; return null;
} }
return new DiscordChannel(token, opts, undefined, DISCORD_OWNER_CHANNEL); return new DiscordChannel(token, opts, undefined, DISCORD_OWNER_CHANNEL);

View File

@@ -396,7 +396,9 @@ describe('session accessors', () => {
const migratedDb = new Database(dbPath, { readonly: true }); const migratedDb = new Database(dbPath, { readonly: true });
expect( expect(
migratedDb 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(), .get(),
).toBeUndefined(); ).toBeUndefined();
migratedDb.close(); migratedDb.close();
@@ -2354,29 +2356,39 @@ describe('service handoff completion', () => {
const migratedDb = new Database(dbPath, { readonly: true }); const migratedDb = new Database(dbPath, { readonly: true });
expect( expect(
(migratedDb (
.prepare(`PRAGMA table_info(work_items)`) migratedDb.prepare(`PRAGMA table_info(work_items)`).all() as Array<{
.all() as Array<{ name: string }>).map((row) => row.name), name: string;
}>
).map((row) => row.name),
).not.toContain('service_id'); ).not.toContain('service_id');
expect( expect(
(migratedDb (
.prepare(`PRAGMA table_info(channel_owner)`) migratedDb.prepare(`PRAGMA table_info(channel_owner)`).all() as Array<{
.all() as Array<{ name: string }>).map((row) => row.name), name: string;
}>
).map((row) => row.name),
).not.toContain('owner_service_id'); ).not.toContain('owner_service_id');
expect( expect(
(migratedDb (
.prepare(`PRAGMA table_info(paired_tasks)`) migratedDb.prepare(`PRAGMA table_info(paired_tasks)`).all() as Array<{
.all() as Array<{ name: string }>).map((row) => row.name), name: string;
}>
).map((row) => row.name),
).not.toContain('owner_service_id'); ).not.toContain('owner_service_id');
expect( expect(
(migratedDb (
.prepare(`PRAGMA table_info(service_handoffs)`) migratedDb
.all() as Array<{ name: string }>).map((row) => row.name), .prepare(`PRAGMA table_info(service_handoffs)`)
.all() as Array<{ name: string }>
).map((row) => row.name),
).not.toContain('source_service_id'); ).not.toContain('source_service_id');
expect( expect(
(migratedDb (
.prepare(`PRAGMA table_info(service_handoffs)`) migratedDb
.all() as Array<{ name: string }>).map((row) => row.name), .prepare(`PRAGMA table_info(service_handoffs)`)
.all() as Array<{ name: string }>
).map((row) => row.name),
).toContain('source_agent_type'); ).toContain('source_agent_type');
migratedDb.close(); migratedDb.close();
}); });
@@ -2493,8 +2505,12 @@ describe('memories', () => {
}); });
expect(recalled).toHaveLength(300); expect(recalled).toHaveLength(300);
expect(recalled.some((memory) => memory.content === 'memory-0')).toBe(false); expect(recalled.some((memory) => memory.content === 'memory-0')).toBe(
expect(recalled.some((memory) => memory.content === 'memory-304')).toBe(true); false,
);
expect(recalled.some((memory) => memory.content === 'memory-304')).toBe(
true,
);
}); });
it('archives stale compact memories before recall using last_used_at TTL', () => { it('archives stale compact memories before recall using last_used_at TTL', () => {
@@ -2526,9 +2542,13 @@ describe('memories', () => {
limit: 10, limit: 10,
}); });
expect(recalled.some((memory) => memory.content === '오래된 compact memory')).toBe(false);
expect( 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); ).toBe(true);
}); });

239
src/db.ts
View File

@@ -179,7 +179,10 @@ export interface ServiceHandoff {
last_error: string | null; last_error: string | null;
} }
interface StoredWorkItemRow extends Omit<WorkItem, 'service_id' | 'agent_type'> { interface StoredWorkItemRow extends Omit<
WorkItem,
'service_id' | 'agent_type'
> {
agent_type: string; agent_type: string;
} }
@@ -198,11 +201,13 @@ interface LegacyChannelOwnerLeaseServiceRow extends StoredChannelOwnerLeaseRow {
arbiter_service_id?: string | null; arbiter_service_id?: string | null;
} }
interface StoredServiceHandoffRow interface StoredServiceHandoffRow extends Omit<
extends Omit< ServiceHandoff,
ServiceHandoff, | 'source_service_id'
'source_service_id' | 'target_service_id' | 'source_agent_type' | 'target_agent_type' | 'target_service_id'
> { | 'source_agent_type'
| 'target_agent_type'
> {
source_agent_type?: string | null; source_agent_type?: string | null;
target_agent_type: string; target_agent_type: string;
} }
@@ -212,15 +217,14 @@ interface LegacyServiceHandoffServiceRow extends StoredServiceHandoffRow {
target_service_id?: string | null; target_service_id?: string | null;
} }
interface StoredPairedTaskRow interface StoredPairedTaskRow extends Omit<
extends Omit< PairedTask,
PairedTask, | 'owner_service_id'
| 'owner_service_id' | 'reviewer_service_id'
| 'reviewer_service_id' | 'owner_agent_type'
| 'owner_agent_type' | 'reviewer_agent_type'
| 'reviewer_agent_type' | 'arbiter_agent_type'
| 'arbiter_agent_type' > {
> {
owner_agent_type?: string | null; owner_agent_type?: string | null;
reviewer_agent_type?: string | null; reviewer_agent_type?: string | null;
arbiter_agent_type?: string | null; arbiter_agent_type?: string | null;
@@ -286,20 +290,19 @@ function hydrateChannelOwnerLeaseRow(
const reviewerAgentType = const reviewerAgentType =
row.reviewer_agent_type == null row.reviewer_agent_type == null
? null ? null
: normalizeStoredAgentType(row.reviewer_agent_type) ?? : (normalizeStoredAgentType(row.reviewer_agent_type) ??
resolveStableReviewerAgentType(ownerAgentType, null); resolveStableReviewerAgentType(ownerAgentType, null));
const arbiterAgentType = const arbiterAgentType =
row.arbiter_agent_type == null row.arbiter_agent_type == null
? null ? null
: normalizeStoredAgentType(row.arbiter_agent_type) ?? : (normalizeStoredAgentType(row.arbiter_agent_type) ??
ARBITER_AGENT_TYPE ?? ARBITER_AGENT_TYPE ??
null; null);
return { return {
chat_jid: row.chat_jid, chat_jid: row.chat_jid,
owner_service_id: owner_service_id:
resolveRoleServiceShadow('owner', ownerAgentType) ?? resolveRoleServiceShadow('owner', ownerAgentType) ?? CLAUDE_SERVICE_ID,
CLAUDE_SERVICE_ID,
reviewer_service_id: reviewer_service_id:
reviewerAgentType == null reviewerAgentType == null
? null ? null
@@ -323,12 +326,15 @@ function hydratePairedTaskRow(row: StoredPairedTaskRow): PairedTask {
row.reviewer_agent_type ?? null, row.reviewer_agent_type ?? null,
); );
const arbiterAgentType = const arbiterAgentType =
normalizeStoredAgentType(row.arbiter_agent_type) ?? ARBITER_AGENT_TYPE ?? null; normalizeStoredAgentType(row.arbiter_agent_type) ??
ARBITER_AGENT_TYPE ??
null;
return { return {
...row, ...row,
owner_service_id: owner_service_id:
resolveRoleServiceShadow('owner', ownerAgentType) ?? CODEX_MAIN_SERVICE_ID, resolveRoleServiceShadow('owner', ownerAgentType) ??
CODEX_MAIN_SERVICE_ID,
reviewer_service_id: reviewer_service_id:
resolveRoleServiceShadow('reviewer', reviewerAgentType) ?? resolveRoleServiceShadow('reviewer', reviewerAgentType) ??
CODEX_REVIEW_SERVICE_ID, 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 = const sourceAgentType =
normalizeStoredAgentType(row.source_agent_type) ?? normalizeStoredAgentType(row.source_agent_type) ??
(row.source_role (row.source_role
@@ -365,22 +373,22 @@ function hydrateServiceHandoffRow(row: StoredServiceHandoffRow): ServiceHandoff
target_agent_type: targetAgentType, target_agent_type: targetAgentType,
source_service_id: source_service_id:
row.source_role != null row.source_role != null
? resolveRoleServiceShadow(row.source_role, sourceAgentType) ?? ? (resolveRoleServiceShadow(row.source_role, sourceAgentType) ??
SERVICE_SESSION_SCOPE SERVICE_SESSION_SCOPE)
: SERVICE_SESSION_SCOPE, : SERVICE_SESSION_SCOPE,
target_service_id: target_service_id:
row.target_role != null row.target_role != null
? resolveRoleServiceShadow(row.target_role, targetAgentType) ?? ? (resolveRoleServiceShadow(row.target_role, targetAgentType) ??
SERVICE_SESSION_SCOPE SERVICE_SESSION_SCOPE)
: SERVICE_SESSION_SCOPE, : SERVICE_SESSION_SCOPE,
}; };
} }
function getTableColumns(database: Database, tableName: string): string[] { function getTableColumns(database: Database, tableName: string): string[] {
return ( return (
database database.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
.prepare(`PRAGMA table_info(${tableName})`) name: string;
.all() as Array<{ name: string }> }>
).map((column) => column.name); ).map((column) => column.name);
} }
@@ -783,7 +791,9 @@ function createSchema(database: Database): void {
} }
try { 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 { } catch {
/* column already exists */ /* column already exists */
} }
@@ -795,13 +805,17 @@ function createSchema(database: Database): void {
} }
try { 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 { } catch {
/* column already exists */ /* column already exists */
} }
try { 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 { } catch {
/* column already exists */ /* column already exists */
} }
@@ -1362,9 +1376,7 @@ function normalizeMemoryKeywords(keywords?: string[]): string[] {
if (!Array.isArray(keywords)) return []; if (!Array.isArray(keywords)) return [];
return [ return [
...new Set( ...new Set(
keywords keywords.map((keyword) => keyword.trim().toLowerCase()).filter(Boolean),
.map((keyword) => keyword.trim().toLowerCase())
.filter(Boolean),
), ),
]; ];
} }
@@ -1453,7 +1465,9 @@ function queryFtsRowOrder(query: RecallMemoryQuery): Map<number, number> {
} }
export function touchMemories(ids: number[]): void { 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; if (uniqueIds.length === 0) return;
const now = new Date().toISOString(); const now = new Date().toISOString();
const stmt = db.prepare( const stmt = db.prepare(
@@ -1573,7 +1587,9 @@ export function rememberMemory(input: {
input.sourceRef ?? null, input.sourceRef ?? null,
createdAt, 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({ expireStaleMemories({
scopeKind: input.scopeKind, scopeKind: input.scopeKind,
scopeKey: input.scopeKey, scopeKey: input.scopeKey,
@@ -1598,8 +1614,9 @@ export function recallMemories(query: RecallMemoryQuery): MemoryRecord[] {
const scored = rows const scored = rows
.map((row, index) => { .map((row, index) => {
const keywords = parseMemoryKeywords(row.keywords_json); const keywords = parseMemoryKeywords(row.keywords_json);
const exactMatches = keywords.filter((keyword) => exactKeywords.has(keyword)) const exactMatches = keywords.filter((keyword) =>
.length; exactKeywords.has(keyword),
).length;
const ftsScore = ftsOrder.get(row.id) ?? 0; const ftsScore = ftsOrder.get(row.id) ?? 0;
const recencyScore = rows.length - index; const recencyScore = rows.length - index;
return { return {
@@ -2016,12 +2033,9 @@ export function getOpenWorkItem(
id ASC id ASC
LIMIT 1`, LIMIT 1`,
) )
.get( .get(chatJid, agentType, preferredRole, preferredRole) as
chatJid, | StoredWorkItemRow
agentType, | undefined;
preferredRole,
preferredRole,
) as StoredWorkItemRow | undefined;
return row ? hydrateWorkItem(row) : 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 } db.prepare('SELECT last_insert_rowid() as id').get() as { id: number }
).id; ).id;
return hydrateWorkItem( return hydrateWorkItem(
db.prepare('SELECT * FROM work_items WHERE id = ?').get( db
lastId, .prepare('SELECT * FROM work_items WHERE id = ?')
) as StoredWorkItemRow, .get(lastId) as StoredWorkItemRow,
); );
} }
@@ -2474,9 +2488,7 @@ export function getSession(
.prepare( .prepare(
'SELECT session_id FROM sessions WHERE group_folder = ? AND agent_type = ?', 'SELECT session_id FROM sessions WHERE group_folder = ? AND agent_type = ?',
) )
.get(groupFolder, agentType) as .get(groupFolder, agentType) as { session_id: string } | undefined;
| { session_id: string }
| undefined;
return row?.session_id; return row?.session_id;
} }
@@ -2613,8 +2625,7 @@ function buildRegisteredGroupFromStoredSettings(
name: stored.name || stored.chatJid, name: stored.name || stored.chatJid,
folder: stored.folder, folder: stored.folder,
trigger: stored.trigger || `@${ASSISTANT_NAME}`, trigger: stored.trigger || `@${ASSISTANT_NAME}`,
added_at: added_at: capabilityMetadata?.added_at || new Date(0).toISOString(),
capabilityMetadata?.added_at || new Date(0).toISOString(),
agentConfig: capabilityMetadata?.agentConfig, agentConfig: capabilityMetadata?.agentConfig,
requiresTrigger: stored.requiresTrigger, requiresTrigger: stored.requiresTrigger,
isMain: stored.isMain ? true : undefined, isMain: stored.isMain ? true : undefined,
@@ -2692,12 +2703,16 @@ export function assignRoom(
const roomMode = input.roomMode || existing?.roomMode || 'single'; const roomMode = input.roomMode || existing?.roomMode || 'single';
const ownerAgentType = const ownerAgentType =
input.ownerAgentType || existing?.ownerAgentType || OWNER_AGENT_TYPE; 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 = { const snapshot: RoomRegistrationSnapshot = {
name: input.name, name: input.name,
folder, folder,
triggerPattern: triggerPattern: input.trigger || existing?.trigger || `@${ASSISTANT_NAME}`,
input.trigger || existing?.trigger || `@${ASSISTANT_NAME}`,
requiresTrigger: input.requiresTrigger ?? existing?.requiresTrigger ?? true, requiresTrigger: input.requiresTrigger ?? existing?.requiresTrigger ?? true,
isMain: input.isMain ?? existing?.isMain ?? false, isMain: input.isMain ?? existing?.isMain ?? false,
ownerAgentType, ownerAgentType,
@@ -2911,7 +2926,9 @@ function getLegacyRegisteredGroup(
'SELECT * FROM registered_groups WHERE jid = ? AND agent_type = ?', 'SELECT * FROM registered_groups WHERE jid = ? AND agent_type = ?',
) )
.get(jid, agentType) .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; ) as RegisteredGroupDatabaseRow | undefined;
return parseRegisteredGroupRow(row); return parseRegisteredGroupRow(row);
} }
@@ -2949,7 +2966,9 @@ function getRegisteredGroupCapabilityMetadata(
}; };
} }
function getStoredRoomRowsFromDatabase(database: Database): StoredRoomSettings[] { function getStoredRoomRowsFromDatabase(
database: Database,
): StoredRoomSettings[] {
return database return database
.prepare( .prepare(
`SELECT chat_jid `SELECT chat_jid
@@ -2958,7 +2977,10 @@ function getStoredRoomRowsFromDatabase(database: Database): StoredRoomSettings[]
) )
.all() .all()
.map((row) => .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)); .filter((row): row is StoredRoomSettings => Boolean(row));
} }
@@ -2995,7 +3017,12 @@ function collectReservedFolders(
FROM registered_groups FROM registered_groups
WHERE ? IS NULL OR jid != ?`, 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; folder: string | null;
}>; }>;
@@ -3042,7 +3069,9 @@ function resolveAssignedRoomFolder(
const reserved = collectReservedFolders(database, chatJid); const reserved = collectReservedFolders(database, chatJid);
if (explicitFolder) { if (explicitFolder) {
if (!isValidGroupFolder(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)) { if (reserved.has(explicitFolder)) {
throw new Error(`Group folder "${explicitFolder}" is already assigned`); throw new Error(`Group folder "${explicitFolder}" is already assigned`);
@@ -3050,7 +3079,10 @@ function resolveAssignedRoomFolder(
return explicitFolder; return explicitFolder;
} }
const existingFolder = getStoredRoomSettingsRowFromDatabase(database, chatJid)?.folder; const existingFolder = getStoredRoomSettingsRowFromDatabase(
database,
chatJid,
)?.folder;
if (existingFolder) return existingFolder; if (existingFolder) return existingFolder;
return buildGeneratedRoomFolder(database, chatJid, name); return buildGeneratedRoomFolder(database, chatJid, name);
@@ -3089,7 +3121,10 @@ function materializeRegisteredGroupsForRoom(
added_at: string; added_at: string;
agent_config: string | null; agent_config: string | null;
}>; }>;
const existingByType = new Map<AgentType, { added_at: string; agent_config: string | null }>(); const existingByType = new Map<
AgentType,
{ added_at: string; agent_config: string | null }
>();
for (const row of existingRows) { for (const row of existingRows) {
const agentType = normalizeStoredAgentType(row.agent_type); const agentType = normalizeStoredAgentType(row.agent_type);
if (agentType) { if (agentType) {
@@ -3101,8 +3136,10 @@ function materializeRegisteredGroupsForRoom(
const existing = existingByType.get(agentType); const existing = existingByType.get(agentType);
const agentConfig = const agentConfig =
agentType === ownerAgentType agentType === ownerAgentType
? ownerAgentConfig ?? ? (ownerAgentConfig ??
(existing?.agent_config ? JSON.parse(existing.agent_config) : undefined) (existing?.agent_config
? JSON.parse(existing.agent_config)
: undefined))
: existing?.agent_config : existing?.agent_config
? JSON.parse(existing.agent_config) ? JSON.parse(existing.agent_config)
: undefined; : undefined;
@@ -3262,9 +3299,8 @@ function collectRoomRegistrationSnapshot(
} }
const agentTypes = collectRegisteredAgentTypes(database, jid); const agentTypes = collectRegisteredAgentTypes(database, jid);
const inferredOwnerAgentType = inferOwnerAgentTypeFromRegisteredAgentTypes( const inferredOwnerAgentType =
agentTypes, inferOwnerAgentTypeFromRegisteredAgentTypes(agentTypes);
);
const preferExplicitTrigger = const preferExplicitTrigger =
existingStored?.modeSource === 'explicit' && existingStored.trigger; existingStored?.modeSource === 'explicit' && existingStored.trigger;
const preferExplicitOwner = const preferExplicitOwner =
@@ -3280,7 +3316,8 @@ function collectRoomRegistrationSnapshot(
: undefined; : undefined;
const inferredOwnerRow = const inferredOwnerRow =
rows.find( rows.find(
(row) => normalizeStoredAgentType(row.agent_type) === inferredOwnerAgentType, (row) =>
normalizeStoredAgentType(row.agent_type) === inferredOwnerAgentType,
) ?? rows[0]; ) ?? rows[0];
const ownerAgentType = preferredOwnerAgentType const ownerAgentType = preferredOwnerAgentType
? preferredOwnerRow ? preferredOwnerRow
@@ -3292,12 +3329,11 @@ function collectRoomRegistrationSnapshot(
return { return {
name: first.name, name: first.name,
folder: first.folder, folder: first.folder,
triggerPattern: triggerPattern: preferExplicitTrigger
preferExplicitTrigger ? existingStored.trigger!
? existingStored.trigger! : preferredOwnerRow != null
: preferredOwnerRow != null ? preferredOwnerRow.trigger_pattern
? preferredOwnerRow.trigger_pattern : ownerRow.trigger_pattern,
: ownerRow.trigger_pattern,
requiresTrigger: (first.requires_trigger ?? 1) === 1, requiresTrigger: (first.requires_trigger ?? 1) === 1,
isMain: (first.is_main ?? 0) === 1, isMain: (first.is_main ?? 0) === 1,
ownerAgentType, ownerAgentType,
@@ -3376,14 +3412,20 @@ function updateStoredRoomMetadata(
function syncStoredRoomRegistrationSnapshotForJid(chatJid: string): void { function syncStoredRoomRegistrationSnapshotForJid(chatJid: string): void {
const existingSettings = getStoredRoomSettingsRowFromDatabase(db, chatJid); const existingSettings = getStoredRoomSettingsRowFromDatabase(db, chatJid);
const snapshot = collectRoomRegistrationSnapshot(db, chatJid, existingSettings); const snapshot = collectRoomRegistrationSnapshot(
db,
chatJid,
existingSettings,
);
if (!snapshot) return; if (!snapshot) return;
if (!existingSettings) { if (!existingSettings) {
insertStoredRoomSettings( insertStoredRoomSettings(
db, db,
chatJid, chatJid,
inferRoomModeFromRegisteredAgentTypes(getRegisteredAgentTypesForJid(chatJid)), inferRoomModeFromRegisteredAgentTypes(
getRegisteredAgentTypesForJid(chatJid),
),
'inferred', 'inferred',
snapshot, snapshot,
); );
@@ -3521,9 +3563,14 @@ function collectRegisteredAgentTypesForFolder(
function resolveStablePairedTaskOwnerAgentType( function resolveStablePairedTaskOwnerAgentType(
database: Database, database: Database,
task: Pick<StoredPairedTaskRow, 'chat_jid' | 'group_folder' | 'owner_agent_type'>, task: Pick<
StoredPairedTaskRow,
'chat_jid' | 'group_folder' | 'owner_agent_type'
>,
): AgentType | undefined { ): AgentType | undefined {
const persistedOwnerAgentType = normalizeStoredAgentType(task.owner_agent_type); const persistedOwnerAgentType = normalizeStoredAgentType(
task.owner_agent_type,
);
if (persistedOwnerAgentType) { if (persistedOwnerAgentType) {
return persistedOwnerAgentType; return persistedOwnerAgentType;
} }
@@ -3729,13 +3776,13 @@ function backfillChannelOwnerRoleMetadata(database: Database): void {
const reviewerServiceId = const reviewerServiceId =
row.reviewer_service_id == null row.reviewer_service_id == null
? null ? null
: resolveRoleServiceShadow('reviewer', reviewerAgentType) ?? : (resolveRoleServiceShadow('reviewer', reviewerAgentType) ??
row.reviewer_service_id; row.reviewer_service_id);
const arbiterServiceId = const arbiterServiceId =
row.arbiter_service_id == null row.arbiter_service_id == null
? null ? null
: resolveRoleServiceShadow('arbiter', arbiterAgentType) ?? : (resolveRoleServiceShadow('arbiter', arbiterAgentType) ??
row.arbiter_service_id; row.arbiter_service_id);
if ( if (
ownerServiceId === row.owner_service_id && ownerServiceId === row.owner_service_id &&
@@ -3911,13 +3958,13 @@ function backfillServiceHandoffServiceShadows(database: Database): void {
const normalizedSourceServiceId = const normalizedSourceServiceId =
row.source_role != null row.source_role != null
? resolveRoleServiceShadow(row.source_role, sourceAgentType) ?? ? (resolveRoleServiceShadow(row.source_role, sourceAgentType) ??
row.source_service_id row.source_service_id)
: row.source_service_id; : row.source_service_id;
const normalizedTargetServiceId = const normalizedTargetServiceId =
row.target_role != null row.target_role != null
? resolveRoleServiceShadow(row.target_role, targetAgentType) ?? ? (resolveRoleServiceShadow(row.target_role, targetAgentType) ??
row.target_service_id row.target_service_id)
: row.target_service_id; : row.target_service_id;
if ( if (
@@ -4624,16 +4671,16 @@ export function setChannelOwnerLease(input: {
const reviewerAgentType = const reviewerAgentType =
input.reviewer_service_id == null && input.reviewer_agent_type == null input.reviewer_service_id == null && input.reviewer_agent_type == null
? null ? null
: normalizeStoredAgentType(input.reviewer_agent_type) ?? : (normalizeStoredAgentType(input.reviewer_agent_type) ??
inferAgentTypeFromServiceShadow(input.reviewer_service_id ?? null) ?? inferAgentTypeFromServiceShadow(input.reviewer_service_id ?? null) ??
resolveStableReviewerAgentType(ownerAgentType, null); resolveStableReviewerAgentType(ownerAgentType, null));
const arbiterAgentType = const arbiterAgentType =
input.arbiter_service_id == null && input.arbiter_agent_type == null input.arbiter_service_id == null && input.arbiter_agent_type == null
? null ? null
: normalizeStoredAgentType(input.arbiter_agent_type) ?? : (normalizeStoredAgentType(input.arbiter_agent_type) ??
inferAgentTypeFromServiceShadow(input.arbiter_service_id ?? null) ?? inferAgentTypeFromServiceShadow(input.arbiter_service_id ?? null) ??
ARBITER_AGENT_TYPE ?? ARBITER_AGENT_TYPE ??
null; null);
db.prepare( db.prepare(
`INSERT OR REPLACE INTO channel_owner ( `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 } db.prepare('SELECT last_insert_rowid() as id').get() as { id: number }
).id; ).id;
return hydrateServiceHandoffRow( return hydrateServiceHandoffRow(
db.prepare('SELECT * FROM service_handoffs WHERE id = ?').get( db
lastId, .prepare('SELECT * FROM service_handoffs WHERE id = ?')
) as StoredServiceHandoffRow, .get(lastId) as StoredServiceHandoffRow,
); );
} }

View File

@@ -894,10 +894,9 @@ describe('assign_room success', () => {
modeSource: 'explicit', modeSource: 'explicit',
name: 'Legacy Tribunal Renamed', name: 'Legacy Tribunal Renamed',
}); });
expect(getRegisteredAgentTypesForJid('legacy-tribunal@g.us').sort()).toEqual([ expect(
'claude-code', getRegisteredAgentTypesForJid('legacy-tribunal@g.us').sort(),
'codex', ).toEqual(['claude-code', 'codex']);
]);
}); });
it('main group can assign a tribunal room and materialize capability rows', async () => { it('main group can assign a tribunal room and materialize capability rows', async () => {

View File

@@ -701,7 +701,10 @@ export async function processTaskIpc(
break; 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( logger.warn(
{ sourceGroup, keywords: data.keywords }, { sourceGroup, keywords: data.keywords },
'Invalid persist_memory request - keywords must be strings', 'Invalid persist_memory request - keywords must be strings',
@@ -717,10 +720,13 @@ export async function processTaskIpc(
memoryKind: memoryKind:
typeof data.memory_kind === 'string' ? data.memory_kind : null, typeof data.memory_kind === 'string' ? data.memory_kind : null,
sourceKind: sourceKind:
(data.source_kind as 'compact' | 'explicit' | 'import' | 'system' | undefined) ?? (data.source_kind as
'compact', | 'compact'
sourceRef: | 'explicit'
typeof data.source_ref === 'string' ? data.source_ref : null, | 'import'
| 'system'
| undefined) ?? 'compact',
sourceRef: typeof data.source_ref === 'string' ? data.source_ref : null,
}); });
logger.info( logger.info(
{ {

View File

@@ -115,8 +115,8 @@ export async function runAgentForGroup(
const inferredRole = resolveActiveRole(pairedTask?.status); const inferredRole = resolveActiveRole(pairedTask?.status);
const canHonorForcedRole = Boolean( const canHonorForcedRole = Boolean(
args.forcedRole === 'owner' || args.forcedRole === 'owner' ||
(args.forcedRole === 'reviewer' && currentLease.reviewer_agent_type) || (args.forcedRole === 'reviewer' && currentLease.reviewer_agent_type) ||
(args.forcedRole === 'arbiter' && currentLease.arbiter_agent_type), (args.forcedRole === 'arbiter' && currentLease.arbiter_agent_type),
); );
const activeRole = canHonorForcedRole ? args.forcedRole! : inferredRole; const activeRole = canHonorForcedRole ? args.forcedRole! : inferredRole;
const effectiveServiceId = resolveLeaseServiceId(currentLease, activeRole); const effectiveServiceId = resolveLeaseServiceId(currentLease, activeRole);

View File

@@ -20,9 +20,7 @@ import {
type ServiceHandoff, type ServiceHandoff,
type WorkItem, type WorkItem,
} from './db.js'; } from './db.js';
import { import { isSessionCommandSenderAllowed } from './config.js';
isSessionCommandSenderAllowed,
} from './config.js';
import { GroupQueue, GroupRunContext } from './group-queue.js'; import { GroupQueue, GroupRunContext } from './group-queue.js';
import { import {
findChannel, findChannel,
@@ -61,9 +59,7 @@ import {
} from './types.js'; } from './types.js';
import { createScopedLogger, logger } from './logger.js'; import { createScopedLogger, logger } from './logger.js';
import { resolveGroupIpcPath } from './group-folder.js'; import { resolveGroupIpcPath } from './group-folder.js';
import { import { hasReviewerLease } from './service-routing.js';
hasReviewerLease,
} from './service-routing.js';
/** /**
* Check if a message is a duplicate of the last bot final message in a paired room. * Check if a message is a duplicate of the last bot final message in a paired room.

View File

@@ -88,7 +88,12 @@ function ownerBranchName(groupFolder: string): string {
} }
function ownerWorkspacePath(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', () => { describe('paired workspace manager', () => {
@@ -739,14 +744,18 @@ describe('paired workspace manager', () => {
groupFolder: 'head-base-room', groupFolder: 'head-base-room',
}); });
const ownerWorkspace = const ownerWorkspace = manager.provisionOwnerWorkspaceForPairedTask(
manager.provisionOwnerWorkspaceForPairedTask('paired-task-head-base'); 'paired-task-head-base',
);
expect( expect(
runGit(['branch', '--show-current'], ownerWorkspace.workspace_dir), runGit(['branch', '--show-current'], ownerWorkspace.workspace_dir),
).toBe(ownerBranchName('head-base-room')); ).toBe(ownerBranchName('head-base-room'));
expect( 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'); ).toBe('feature base\n');
expect(runGit(['rev-parse', 'HEAD'], ownerWorkspace.workspace_dir)).toBe( expect(runGit(['rev-parse', 'HEAD'], ownerWorkspace.workspace_dir)).toBe(
runGit(['rev-parse', 'HEAD'], canonicalDir), runGit(['rev-parse', 'HEAD'], canonicalDir),
@@ -781,7 +790,10 @@ describe('paired workspace manager', () => {
runGit(['branch', '--show-current'], secondProvision.workspace_dir), runGit(['branch', '--show-current'], secondProvision.workspace_dir),
).toBe(ownerBranchName('tree-source-room')); ).toBe(ownerBranchName('tree-source-room'));
expect( 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'); ).toBe('tree source\n');
expect(runGit(['rev-parse', 'HEAD'], secondProvision.workspace_dir)).toBe( expect(runGit(['rev-parse', 'HEAD'], secondProvision.workspace_dir)).toBe(
runGit(['rev-parse', 'HEAD'], canonicalDir), runGit(['rev-parse', 'HEAD'], canonicalDir),
@@ -814,9 +826,9 @@ describe('paired workspace manager', () => {
expect( expect(
runGit(['branch', '--show-current'], reprovisioned.workspace_dir), runGit(['branch', '--show-current'], reprovisioned.workspace_dir),
).toBe(ownerBranchName('repair-room')); ).toBe(ownerBranchName('repair-room'));
expect( expect(runGit(['worktree', 'list', '--porcelain'], canonicalDir)).toContain(
runGit(['worktree', 'list', '--porcelain'], canonicalDir), reprovisioned.workspace_dir,
).toContain(reprovisioned.workspace_dir); );
}); });
it('lazy-migrates a detached dirty owner workspace to a new channel branch', async () => { 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), runGit(['branch', '--show-current'], ownerWorkspace.workspace_dir),
).toBe(ownerBranchName('migrate-room')); ).toBe(ownerBranchName('migrate-room'));
expect( 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'); ).toBe('detached dirty\n');
expect( 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'); ).toBe('new file\n');
expect(runGit(['status', '--short'], ownerWorkspace.workspace_dir)).toContain( expect(
'M README.md', runGit(['status', '--short'], ownerWorkspace.workspace_dir),
); ).toContain('M README.md');
expect(runGit(['status', '--short'], ownerWorkspace.workspace_dir)).toContain( expect(
'?? NEW_FILE.txt', 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 () => { 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'); const workspaceDir = ownerWorkspacePath('migrate-existing-room');
fs.mkdirSync(path.dirname(workspaceDir), { recursive: true }); fs.mkdirSync(path.dirname(workspaceDir), { recursive: true });
runGit(['worktree', 'add', workspaceDir, 'HEAD'], canonicalDir); 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'); fs.writeFileSync(path.join(workspaceDir, 'NOTES.md'), 'keep me\n');
const ownerWorkspace = manager.provisionOwnerWorkspaceForPairedTask( const ownerWorkspace = manager.provisionOwnerWorkspaceForPairedTask(
@@ -886,10 +907,16 @@ describe('paired workspace manager', () => {
runGit(['branch', '--show-current'], ownerWorkspace.workspace_dir), runGit(['branch', '--show-current'], ownerWorkspace.workspace_dir),
).toBe(ownerBranchName('migrate-existing-room')); ).toBe(ownerBranchName('migrate-existing-room'));
expect( 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'); ).toBe('detached existing\n');
expect( 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'); ).toBe('keep me\n');
}); });

View File

@@ -588,7 +588,14 @@ export function provisionOwnerWorkspaceForPairedTask(
runGit(['worktree', 'add', workspaceDir, targetBranch], canonicalWorkDir); runGit(['worktree', 'add', workspaceDir, targetBranch], canonicalWorkDir);
} else { } else {
runGit( runGit(
['worktree', 'add', '-b', targetBranch, workspaceDir, canonicalHeadCommit], [
'worktree',
'add',
'-b',
targetBranch,
workspaceDir,
canonicalHeadCommit,
],
canonicalWorkDir, canonicalWorkDir,
); );
} }

View File

@@ -55,14 +55,14 @@ function normalizeLeaseRow(
const reviewerAgentType = const reviewerAgentType =
row.reviewer_service_id == null row.reviewer_service_id == null
? null ? null
: row.reviewer_agent_type ?? : (row.reviewer_agent_type ??
inferAgentTypeFromServiceShadow(row.reviewer_service_id) ?? inferAgentTypeFromServiceShadow(row.reviewer_service_id) ??
resolveRoleAgentPlan({ resolveRoleAgentPlan({
paired: true, paired: true,
groupAgentType: ownerAgentType, groupAgentType: ownerAgentType,
configuredReviewer: REVIEWER_AGENT_TYPE, configuredReviewer: REVIEWER_AGENT_TYPE,
configuredArbiter: ARBITER_AGENT_TYPE, configuredArbiter: ARBITER_AGENT_TYPE,
}).reviewerAgentType; }).reviewerAgentType);
const arbiterAgentType = const arbiterAgentType =
row.arbiter_agent_type ?? row.arbiter_agent_type ??
(row.arbiter_service_id (row.arbiter_service_id
@@ -79,12 +79,14 @@ function normalizeLeaseRow(
resolveRoleServiceShadow('owner', ownerAgentType) ?? resolveRoleServiceShadow('owner', ownerAgentType) ??
normalizeServiceId(row.owner_service_id), normalizeServiceId(row.owner_service_id),
reviewer_service_id: reviewerAgentType reviewer_service_id: reviewerAgentType
? resolveRoleServiceShadow('reviewer', reviewerAgentType) ?? ? (resolveRoleServiceShadow('reviewer', reviewerAgentType) ??
normalizeServiceId(row.reviewer_service_id!) normalizeServiceId(row.reviewer_service_id!))
: null, : null,
arbiter_service_id: arbiterAgentType arbiter_service_id: arbiterAgentType
? resolveRoleServiceShadow('arbiter', arbiterAgentType) ?? ? (resolveRoleServiceShadow('arbiter', arbiterAgentType) ??
(row.arbiter_service_id ? normalizeServiceId(row.arbiter_service_id) : null) (row.arbiter_service_id
? normalizeServiceId(row.arbiter_service_id)
: null))
: row.arbiter_service_id : row.arbiter_service_id
? normalizeServiceId(row.arbiter_service_id) ? normalizeServiceId(row.arbiter_service_id)
: isArbiterEnabled() : isArbiterEnabled()
@@ -218,19 +220,19 @@ export function resolveLeaseServiceId(
); );
case 'reviewer': case 'reviewer':
return lease.reviewer_agent_type return lease.reviewer_agent_type
? resolveRoleServiceShadow('reviewer', lease.reviewer_agent_type) ?? ? (resolveRoleServiceShadow('reviewer', lease.reviewer_agent_type) ??
(lease.reviewer_service_id (lease.reviewer_service_id
? normalizeServiceId(lease.reviewer_service_id) ? normalizeServiceId(lease.reviewer_service_id)
: null) : null))
: lease.reviewer_service_id : lease.reviewer_service_id
? normalizeServiceId(lease.reviewer_service_id) ? normalizeServiceId(lease.reviewer_service_id)
: null; : null;
case 'arbiter': case 'arbiter':
return lease.arbiter_agent_type return lease.arbiter_agent_type
? resolveRoleServiceShadow('arbiter', lease.arbiter_agent_type) ?? ? (resolveRoleServiceShadow('arbiter', lease.arbiter_agent_type) ??
(lease.arbiter_service_id (lease.arbiter_service_id
? normalizeServiceId(lease.arbiter_service_id) ? normalizeServiceId(lease.arbiter_service_id)
: null) : null))
: lease.arbiter_service_id : lease.arbiter_service_id
? normalizeServiceId(lease.arbiter_service_id) ? normalizeServiceId(lease.arbiter_service_id)
: null; : null;
@@ -242,7 +244,9 @@ export function isOwnerServiceForChat(
serviceId: string = SERVICE_ID, serviceId: string = SERVICE_ID,
): boolean { ): boolean {
const lease = getEffectiveChannelLease(chatJid); const lease = getEffectiveChannelLease(chatJid);
return normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'owner'); return (
normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'owner')
);
} }
export function isReviewerServiceForChat( export function isReviewerServiceForChat(
@@ -250,11 +254,16 @@ export function isReviewerServiceForChat(
serviceId: string = SERVICE_ID, serviceId: string = SERVICE_ID,
): boolean { ): boolean {
const lease = getEffectiveChannelLease(chatJid); const lease = getEffectiveChannelLease(chatJid);
return normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'reviewer'); return (
normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'reviewer')
);
} }
export function hasReviewerLease(chatJid: string): boolean { export function hasReviewerLease(chatJid: string): boolean {
return resolveLeaseServiceId(getEffectiveChannelLease(chatJid), 'reviewer') !== null; return (
resolveLeaseServiceId(getEffectiveChannelLease(chatJid), 'reviewer') !==
null
);
} }
export function isArbiterServiceForChat( export function isArbiterServiceForChat(
@@ -262,7 +271,9 @@ export function isArbiterServiceForChat(
serviceId: string = SERVICE_ID, serviceId: string = SERVICE_ID,
): boolean { ): boolean {
const lease = getEffectiveChannelLease(chatJid); const lease = getEffectiveChannelLease(chatJid);
return normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'arbiter'); return (
normalizeServiceId(serviceId) === resolveLeaseServiceId(lease, 'arbiter')
);
} }
export function shouldServiceProcessChat( export function shouldServiceProcessChat(

View File

@@ -35,7 +35,8 @@ describe('sqlite-memory-store helpers', () => {
id: 2, id: 2,
scopeKind: 'room', scopeKind: 'room',
scopeKey: 'room:ejclaw', scopeKey: 'room:ejclaw',
content: '자동 recall/compact persist를 호스트가 책임지는 방향으로 합의함.', content:
'자동 recall/compact persist를 호스트가 책임지는 방향으로 합의함.',
keywords: ['room:ejclaw'], keywords: ['room:ejclaw'],
memoryKind: null, memoryKind: null,
sourceKind: 'compact', sourceKind: 'compact',

View File

@@ -138,11 +138,7 @@ vi.mock('./service-routing.js', () => ({
hasReviewerLease: vi.fn(() => false), hasReviewerLease: vi.fn(() => false),
})); }));
import { import { _initTestDatabase, createTask, getTaskById } from './db.js';
_initTestDatabase,
createTask,
getTaskById,
} from './db.js';
import * as codexTokenRotation from './codex-token-rotation.js'; import * as codexTokenRotation from './codex-token-rotation.js';
import { TIMEZONE } from './config.js'; import { TIMEZONE } from './config.js';
import * as serviceRouting from './service-routing.js'; import * as serviceRouting from './service-routing.js';

View File

@@ -757,7 +757,11 @@ async function runGithubCiTask(
if (check.terminal) { if (check.terminal) {
await statusTracker.update('completed'); await statusTracker.update('completed');
if (check.completionMessage) { if (check.completionMessage) {
await sendScheduledMessage(deps, task.chat_jid, check.completionMessage); await sendScheduledMessage(
deps,
task.chat_jid,
check.completionMessage,
);
} }
deleteTask(task.id); deleteTask(task.id);
completedAndDeleted = true; completedAndDeleted = true;