feat: backfill room-level settings snapshot
This commit is contained in:
174
src/db.test.ts
174
src/db.test.ts
@@ -36,6 +36,7 @@ import {
|
||||
getPairedWorkspace,
|
||||
getRouterStateForService,
|
||||
getSession,
|
||||
getStoredRoomSettings,
|
||||
getTaskById,
|
||||
listPairedWorkspacesForTask,
|
||||
markWorkItemDelivered,
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
setExplicitRoomMode,
|
||||
storeChatMetadata,
|
||||
storeMessage,
|
||||
updateRegisteredGroupName,
|
||||
updatePairedTask,
|
||||
upsertPairedProject,
|
||||
upsertPairedWorkspace,
|
||||
@@ -748,16 +750,16 @@ describe('registered group isMain', () => {
|
||||
|
||||
it('filters duplicate jid registrations by agent type', () => {
|
||||
setRegisteredGroup('dc:shared', {
|
||||
name: 'Shared Room Claude',
|
||||
name: 'Shared Room',
|
||||
folder: 'shared-room',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
setRegisteredGroup('dc:shared', {
|
||||
name: 'Shared Room Codex',
|
||||
name: 'Shared Room',
|
||||
folder: 'shared-room',
|
||||
trigger: '@Andy',
|
||||
trigger: '@Codex',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
});
|
||||
@@ -766,24 +768,24 @@ describe('registered group isMain', () => {
|
||||
const codexGroups = getAllRegisteredGroups('codex');
|
||||
|
||||
expect(claudeGroups['dc:shared']?.agentType).toBe('claude-code');
|
||||
expect(claudeGroups['dc:shared']?.name).toBe('Shared Room Claude');
|
||||
expect(claudeGroups['dc:shared']?.name).toBe('Shared Room');
|
||||
expect(codexGroups['dc:shared']?.agentType).toBe('codex');
|
||||
expect(codexGroups['dc:shared']?.name).toBe('Shared Room Codex');
|
||||
expect(codexGroups['dc:shared']?.name).toBe('Shared Room');
|
||||
});
|
||||
});
|
||||
|
||||
describe('paired room registration', () => {
|
||||
it('detects when both Claude and Codex are registered on the same jid', () => {
|
||||
setRegisteredGroup('dc:123', {
|
||||
name: 'Paired Room Claude',
|
||||
folder: 'paired-claude',
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
setRegisteredGroup('dc:123', {
|
||||
name: 'Paired Room Codex',
|
||||
folder: 'paired-codex',
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@Codex',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
@@ -813,15 +815,15 @@ describe('paired room registration', () => {
|
||||
|
||||
it('keeps inferred room mode available when no explicit override exists', () => {
|
||||
setRegisteredGroup('dc:legacy-paired', {
|
||||
name: 'Legacy Paired Claude',
|
||||
folder: 'legacy-paired-claude',
|
||||
name: 'Legacy Paired',
|
||||
folder: 'legacy-paired',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
setRegisteredGroup('dc:legacy-paired', {
|
||||
name: 'Legacy Paired Codex',
|
||||
folder: 'legacy-paired-codex',
|
||||
name: 'Legacy Paired',
|
||||
folder: 'legacy-paired',
|
||||
trigger: '@Codex',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
@@ -876,16 +878,16 @@ describe('paired room registration', () => {
|
||||
);
|
||||
insertGroup.run(
|
||||
'dc:legacy-sql',
|
||||
'Legacy SQL Claude',
|
||||
'legacy-sql-claude',
|
||||
'@Andy',
|
||||
'Legacy SQL Room',
|
||||
'legacy-sql-room',
|
||||
'@Claude',
|
||||
'2024-01-01T00:00:00.000Z',
|
||||
'claude-code',
|
||||
);
|
||||
insertGroup.run(
|
||||
'dc:legacy-sql',
|
||||
'Legacy SQL Codex',
|
||||
'legacy-sql-codex',
|
||||
'Legacy SQL Room',
|
||||
'legacy-sql-room',
|
||||
'@Codex',
|
||||
'2024-01-01T00:00:00.000Z',
|
||||
'codex',
|
||||
@@ -897,19 +899,147 @@ describe('paired room registration', () => {
|
||||
expect(getExplicitRoomMode('dc:legacy-sql')).toBeUndefined();
|
||||
expect(getEffectiveRoomMode('dc:legacy-sql')).toBe('tribunal');
|
||||
expect(getEffectiveRuntimeRoomMode('dc:legacy-sql')).toBe('tribunal');
|
||||
|
||||
expect(getStoredRoomSettings('dc:legacy-sql')).toMatchObject({
|
||||
chatJid: 'dc:legacy-sql',
|
||||
roomMode: 'tribunal',
|
||||
modeSource: 'inferred',
|
||||
name: 'Legacy SQL Room',
|
||||
folder: 'legacy-sql-room',
|
||||
trigger: '@Codex',
|
||||
requiresTrigger: true,
|
||||
isMain: false,
|
||||
ownerAgentType: 'codex',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps room-level metadata synced on legacy register_group writes', () => {
|
||||
setRegisteredGroup('dc:room-settings', {
|
||||
name: 'Room Settings Test',
|
||||
folder: 'room-settings-test',
|
||||
trigger: '@Claude',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
setRegisteredGroup('dc:room-settings', {
|
||||
name: 'Room Settings Test',
|
||||
folder: 'room-settings-test',
|
||||
trigger: '@Codex',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
});
|
||||
|
||||
expect(getStoredRoomSettings('dc:room-settings')).toMatchObject({
|
||||
chatJid: 'dc:room-settings',
|
||||
roomMode: 'tribunal',
|
||||
modeSource: 'inferred',
|
||||
name: 'Room Settings Test',
|
||||
folder: 'room-settings-test',
|
||||
trigger: '@Codex',
|
||||
ownerAgentType: 'codex',
|
||||
});
|
||||
|
||||
setExplicitRoomMode('dc:room-settings', 'single');
|
||||
|
||||
expect(getStoredRoomSettings('dc:room-settings')).toMatchObject({
|
||||
chatJid: 'dc:room-settings',
|
||||
roomMode: 'single',
|
||||
modeSource: 'explicit',
|
||||
name: 'Room Settings Test',
|
||||
folder: 'room-settings-test',
|
||||
trigger: '@Codex',
|
||||
ownerAgentType: 'codex',
|
||||
});
|
||||
|
||||
updateRegisteredGroupName('dc:room-settings', 'Room Settings Renamed');
|
||||
|
||||
expect(getStoredRoomSettings('dc:room-settings')).toMatchObject({
|
||||
chatJid: 'dc:room-settings',
|
||||
roomMode: 'single',
|
||||
modeSource: 'explicit',
|
||||
name: 'Room Settings Renamed',
|
||||
folder: 'room-settings-test',
|
||||
trigger: '@Codex',
|
||||
ownerAgentType: 'codex',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails room_settings backfill when room-level metadata conflicts across agent rows', () => {
|
||||
const tempDir = fs.mkdtempSync('/tmp/ejclaw-room-settings-conflict-');
|
||||
const dbPath = path.join(tempDir, 'messages.db');
|
||||
const legacyDb = new Database(dbPath);
|
||||
|
||||
legacyDb.exec(`
|
||||
CREATE TABLE registered_groups (
|
||||
jid TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
folder TEXT NOT NULL,
|
||||
trigger_pattern TEXT NOT NULL,
|
||||
added_at TEXT NOT NULL,
|
||||
agent_config TEXT,
|
||||
requires_trigger INTEGER DEFAULT 1,
|
||||
is_main INTEGER DEFAULT 0,
|
||||
agent_type TEXT NOT NULL DEFAULT 'claude-code',
|
||||
work_dir TEXT,
|
||||
PRIMARY KEY (jid, agent_type),
|
||||
UNIQUE (folder, agent_type)
|
||||
);
|
||||
CREATE TABLE room_settings (
|
||||
chat_jid TEXT PRIMARY KEY,
|
||||
room_mode TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
CHECK (room_mode IN ('single', 'tribunal'))
|
||||
);
|
||||
`);
|
||||
|
||||
const insertGroup = legacyDb.prepare(
|
||||
`INSERT INTO registered_groups (
|
||||
jid,
|
||||
name,
|
||||
folder,
|
||||
trigger_pattern,
|
||||
added_at,
|
||||
agent_config,
|
||||
requires_trigger,
|
||||
is_main,
|
||||
agent_type,
|
||||
work_dir
|
||||
) VALUES (?, ?, ?, ?, ?, NULL, 1, 0, ?, NULL)`,
|
||||
);
|
||||
insertGroup.run(
|
||||
'dc:conflict',
|
||||
'Conflict Room',
|
||||
'conflict-folder-1',
|
||||
'@Claude',
|
||||
'2024-01-01T00:00:00.000Z',
|
||||
'claude-code',
|
||||
);
|
||||
insertGroup.run(
|
||||
'dc:conflict',
|
||||
'Conflict Room',
|
||||
'conflict-folder-2',
|
||||
'@Codex',
|
||||
'2024-01-01T00:00:00.000Z',
|
||||
'codex',
|
||||
);
|
||||
legacyDb.close();
|
||||
|
||||
expect(() => _initTestDatabaseFromFile(dbPath)).toThrow(
|
||||
/Conflicting room-level registered_groups metadata/,
|
||||
);
|
||||
});
|
||||
|
||||
it('lets explicit single override dual registration for paired-room checks', () => {
|
||||
setRegisteredGroup('dc:explicit-single', {
|
||||
name: 'Explicit Single Claude',
|
||||
folder: 'explicit-single-claude',
|
||||
name: 'Explicit Single',
|
||||
folder: 'explicit-single',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
setRegisteredGroup('dc:explicit-single', {
|
||||
name: 'Explicit Single Codex',
|
||||
folder: 'explicit-single-codex',
|
||||
name: 'Explicit Single',
|
||||
folder: 'explicit-single',
|
||||
trigger: '@Codex',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
|
||||
398
src/db.ts
398
src/db.ts
@@ -9,6 +9,7 @@ import {
|
||||
CODEX_REVIEW_SERVICE_ID,
|
||||
DATA_DIR,
|
||||
normalizeServiceId,
|
||||
OWNER_AGENT_TYPE,
|
||||
REVIEWER_AGENT_TYPE,
|
||||
SERVICE_ID,
|
||||
SERVICE_SESSION_SCOPE,
|
||||
@@ -45,6 +46,29 @@ interface StoredRoomModeRow {
|
||||
source: RoomModeSource;
|
||||
}
|
||||
|
||||
export interface StoredRoomSettings {
|
||||
chatJid: string;
|
||||
roomMode: RoomMode;
|
||||
modeSource: RoomModeSource;
|
||||
name?: string;
|
||||
folder?: string;
|
||||
trigger?: string;
|
||||
requiresTrigger?: boolean;
|
||||
isMain?: boolean;
|
||||
ownerAgentType?: AgentType;
|
||||
workDir?: string;
|
||||
}
|
||||
|
||||
interface RoomRegistrationSnapshot {
|
||||
name: string;
|
||||
folder: string;
|
||||
triggerPattern: string;
|
||||
requiresTrigger: boolean;
|
||||
isMain: boolean;
|
||||
ownerAgentType: AgentType;
|
||||
workDir: string | null;
|
||||
}
|
||||
|
||||
export interface WorkItem {
|
||||
id: number;
|
||||
group_folder: string;
|
||||
@@ -310,8 +334,16 @@ function createSchema(database: Database): void {
|
||||
chat_jid TEXT PRIMARY KEY,
|
||||
room_mode TEXT NOT NULL,
|
||||
mode_source TEXT NOT NULL DEFAULT 'explicit',
|
||||
name TEXT,
|
||||
folder TEXT,
|
||||
trigger_pattern TEXT,
|
||||
requires_trigger INTEGER DEFAULT 1,
|
||||
is_main INTEGER DEFAULT 0,
|
||||
owner_agent_type TEXT,
|
||||
work_dir TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
CHECK (room_mode IN ('single', 'tribunal'))
|
||||
CHECK (room_mode IN ('single', 'tribunal')),
|
||||
CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS service_handoffs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -402,6 +434,52 @@ function createSchema(database: Database): void {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
try {
|
||||
database.exec(`ALTER TABLE room_settings ADD COLUMN name TEXT`);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
try {
|
||||
database.exec(`ALTER TABLE room_settings ADD COLUMN folder TEXT`);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
try {
|
||||
database.exec(`ALTER TABLE room_settings ADD COLUMN trigger_pattern TEXT`);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
try {
|
||||
database.exec(
|
||||
`ALTER TABLE room_settings ADD COLUMN requires_trigger INTEGER DEFAULT 1`,
|
||||
);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
try {
|
||||
database.exec(
|
||||
`ALTER TABLE room_settings ADD COLUMN is_main INTEGER DEFAULT 0`,
|
||||
);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
try {
|
||||
database.exec(`ALTER TABLE room_settings ADD COLUMN owner_agent_type TEXT`);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
try {
|
||||
database.exec(`ALTER TABLE room_settings ADD COLUMN work_dir TEXT`);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
database.exec(
|
||||
`UPDATE room_settings
|
||||
SET mode_source = 'explicit'
|
||||
@@ -807,7 +885,7 @@ function createSchema(database: Database): void {
|
||||
/* columns already exist */
|
||||
}
|
||||
|
||||
backfillStoredRoomModes(database);
|
||||
backfillStoredRoomSettings(database);
|
||||
}
|
||||
|
||||
export function initDatabase(): void {
|
||||
@@ -1859,25 +1937,29 @@ export function setRegisteredGroup(jid: string, group: RegisteredGroup): void {
|
||||
if (!isValidGroupFolder(group.folder)) {
|
||||
throw new Error(`Invalid group folder "${group.folder}" for JID ${jid}`);
|
||||
}
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups (jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main, agent_type, work_dir)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
jid,
|
||||
group.name,
|
||||
group.folder,
|
||||
group.trigger,
|
||||
group.added_at,
|
||||
group.agentConfig ? JSON.stringify(group.agentConfig) : null,
|
||||
group.requiresTrigger === undefined ? 1 : group.requiresTrigger ? 1 : 0,
|
||||
group.isMain ? 1 : 0,
|
||||
group.agentType || 'claude-code',
|
||||
group.workDir || null,
|
||||
);
|
||||
const tx = db.transaction(() => {
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups (jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main, agent_type, work_dir)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
jid,
|
||||
group.name,
|
||||
group.folder,
|
||||
group.trigger,
|
||||
group.added_at,
|
||||
group.agentConfig ? JSON.stringify(group.agentConfig) : null,
|
||||
group.requiresTrigger === undefined ? 1 : group.requiresTrigger ? 1 : 0,
|
||||
group.isMain ? 1 : 0,
|
||||
group.agentType || 'claude-code',
|
||||
group.workDir || null,
|
||||
);
|
||||
|
||||
if (!existingRoomMode || existingRoomMode.source === 'inferred') {
|
||||
syncInferredRoomModeForJid(jid);
|
||||
}
|
||||
syncStoredRoomRegistrationSnapshotForJid(jid);
|
||||
if (!existingRoomMode || existingRoomMode.source === 'inferred') {
|
||||
syncInferredRoomModeForJid(jid);
|
||||
}
|
||||
});
|
||||
tx();
|
||||
}
|
||||
|
||||
export function updateRegisteredGroupName(jid: string, name: string): void {
|
||||
@@ -1885,6 +1967,7 @@ export function updateRegisteredGroupName(jid: string, name: string): void {
|
||||
name,
|
||||
jid,
|
||||
);
|
||||
syncStoredRoomRegistrationSnapshotForJid(jid);
|
||||
}
|
||||
|
||||
export function getAllRegisteredGroups(
|
||||
@@ -1974,6 +2057,15 @@ export function inferRoomModeFromRegisteredAgentTypes(
|
||||
return types.has('claude-code') && types.has('codex') ? 'tribunal' : 'single';
|
||||
}
|
||||
|
||||
function inferOwnerAgentTypeFromRegisteredAgentTypes(
|
||||
agentTypes: readonly AgentType[],
|
||||
): AgentType {
|
||||
const types = new Set(agentTypes);
|
||||
if (types.has(OWNER_AGENT_TYPE)) return OWNER_AGENT_TYPE;
|
||||
if (types.has('codex')) return 'codex';
|
||||
return 'claude-code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal registration/backfill helper.
|
||||
* This infers the stored room mode from current registrations and must not be
|
||||
@@ -1991,24 +2083,228 @@ function normalizeRoomModeSource(
|
||||
return source === 'explicit' || source === 'inferred' ? source : undefined;
|
||||
}
|
||||
|
||||
function getStoredRoomModeRow(chatJid: string): StoredRoomModeRow | undefined {
|
||||
if (!db) return undefined;
|
||||
function normalizeStoredAgentType(
|
||||
agentType: string | null | undefined,
|
||||
): AgentType | undefined {
|
||||
return agentType === 'claude-code' || agentType === 'codex'
|
||||
? agentType
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const row = db
|
||||
function getStoredRoomSettingsRowFromDatabase(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
): StoredRoomSettings | undefined {
|
||||
const row = database
|
||||
.prepare(
|
||||
`SELECT room_mode, mode_source
|
||||
`SELECT room_mode, mode_source, name, folder, trigger_pattern,
|
||||
requires_trigger, is_main, owner_agent_type, work_dir
|
||||
FROM room_settings
|
||||
WHERE chat_jid = ?`,
|
||||
)
|
||||
.get(chatJid) as
|
||||
| { room_mode: string | null; mode_source: string | null }
|
||||
| {
|
||||
room_mode: string | null;
|
||||
mode_source: string | null;
|
||||
name: string | null;
|
||||
folder: string | null;
|
||||
trigger_pattern: string | null;
|
||||
requires_trigger: number | null;
|
||||
is_main: number | null;
|
||||
owner_agent_type: string | null;
|
||||
work_dir: string | null;
|
||||
}
|
||||
| undefined;
|
||||
const roomMode =
|
||||
row?.room_mode === 'single' || row?.room_mode === 'tribunal'
|
||||
? row.room_mode
|
||||
: undefined;
|
||||
const source = normalizeRoomModeSource(row?.mode_source);
|
||||
return roomMode && source ? { roomMode, source } : undefined;
|
||||
if (!row || !roomMode || !source) return undefined;
|
||||
|
||||
return {
|
||||
chatJid,
|
||||
roomMode,
|
||||
modeSource: source,
|
||||
name: row.name ?? undefined,
|
||||
folder: row.folder ?? undefined,
|
||||
trigger: row.trigger_pattern ?? undefined,
|
||||
requiresTrigger:
|
||||
row.requires_trigger === null ? undefined : row.requires_trigger === 1,
|
||||
isMain: row.is_main === null ? undefined : row.is_main === 1,
|
||||
ownerAgentType: normalizeStoredAgentType(row.owner_agent_type),
|
||||
workDir: row.work_dir ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function getStoredRoomSettings(
|
||||
chatJid: string,
|
||||
): StoredRoomSettings | undefined {
|
||||
if (!db) return undefined;
|
||||
return getStoredRoomSettingsRowFromDatabase(db, chatJid);
|
||||
}
|
||||
|
||||
function getStoredRoomModeRow(chatJid: string): StoredRoomModeRow | undefined {
|
||||
const row = getStoredRoomSettings(chatJid);
|
||||
return row
|
||||
? {
|
||||
roomMode: row.roomMode,
|
||||
source: row.modeSource,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function collectRoomRegistrationSnapshot(
|
||||
database: Database,
|
||||
jid: string,
|
||||
): RoomRegistrationSnapshot | undefined {
|
||||
const rows = database
|
||||
.prepare(
|
||||
`SELECT name, folder, trigger_pattern, requires_trigger, is_main, agent_type, work_dir
|
||||
FROM registered_groups
|
||||
WHERE jid = ?
|
||||
ORDER BY agent_type`,
|
||||
)
|
||||
.all(jid) as Array<{
|
||||
name: string;
|
||||
folder: string;
|
||||
trigger_pattern: string;
|
||||
requires_trigger: number | null;
|
||||
is_main: number | null;
|
||||
agent_type: string | null;
|
||||
work_dir: string | null;
|
||||
}>;
|
||||
|
||||
if (rows.length === 0) return undefined;
|
||||
|
||||
const first = rows[0];
|
||||
const conflicts = new Set<string>();
|
||||
for (const row of rows.slice(1)) {
|
||||
if (row.name !== first.name) conflicts.add('name');
|
||||
if (row.folder !== first.folder) conflicts.add('folder');
|
||||
if ((row.requires_trigger ?? 1) !== (first.requires_trigger ?? 1)) {
|
||||
conflicts.add('requires_trigger');
|
||||
}
|
||||
if ((row.is_main ?? 0) !== (first.is_main ?? 0)) {
|
||||
conflicts.add('is_main');
|
||||
}
|
||||
if ((row.work_dir ?? null) !== (first.work_dir ?? null)) {
|
||||
conflicts.add('work_dir');
|
||||
}
|
||||
}
|
||||
|
||||
if (conflicts.size > 0) {
|
||||
throw new Error(
|
||||
`Conflicting room-level registered_groups metadata for ${jid}: ${[
|
||||
...conflicts,
|
||||
].join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const agentTypes = collectRegisteredAgentTypes(database, jid);
|
||||
const ownerAgentType = inferOwnerAgentTypeFromRegisteredAgentTypes(agentTypes);
|
||||
const ownerRow =
|
||||
rows.find(
|
||||
(row) => normalizeStoredAgentType(row.agent_type) === ownerAgentType,
|
||||
) ?? rows[0];
|
||||
|
||||
return {
|
||||
name: first.name,
|
||||
folder: first.folder,
|
||||
triggerPattern: ownerRow.trigger_pattern,
|
||||
requiresTrigger: (first.requires_trigger ?? 1) === 1,
|
||||
isMain: (first.is_main ?? 0) === 1,
|
||||
ownerAgentType,
|
||||
workDir: first.work_dir ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function insertStoredRoomSettings(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
roomMode: RoomMode,
|
||||
source: RoomModeSource,
|
||||
snapshot: RoomRegistrationSnapshot,
|
||||
): void {
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO room_settings (
|
||||
chat_jid,
|
||||
room_mode,
|
||||
mode_source,
|
||||
name,
|
||||
folder,
|
||||
trigger_pattern,
|
||||
requires_trigger,
|
||||
is_main,
|
||||
owner_agent_type,
|
||||
work_dir,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
chatJid,
|
||||
roomMode,
|
||||
source,
|
||||
snapshot.name,
|
||||
snapshot.folder,
|
||||
snapshot.triggerPattern,
|
||||
snapshot.requiresTrigger ? 1 : 0,
|
||||
snapshot.isMain ? 1 : 0,
|
||||
snapshot.ownerAgentType,
|
||||
snapshot.workDir,
|
||||
new Date().toISOString(),
|
||||
);
|
||||
}
|
||||
|
||||
function updateStoredRoomMetadata(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
snapshot: RoomRegistrationSnapshot,
|
||||
): void {
|
||||
database
|
||||
.prepare(
|
||||
`UPDATE room_settings
|
||||
SET name = ?,
|
||||
folder = ?,
|
||||
trigger_pattern = ?,
|
||||
requires_trigger = ?,
|
||||
is_main = ?,
|
||||
owner_agent_type = ?,
|
||||
work_dir = ?,
|
||||
updated_at = ?
|
||||
WHERE chat_jid = ?`,
|
||||
)
|
||||
.run(
|
||||
snapshot.name,
|
||||
snapshot.folder,
|
||||
snapshot.triggerPattern,
|
||||
snapshot.requiresTrigger ? 1 : 0,
|
||||
snapshot.isMain ? 1 : 0,
|
||||
snapshot.ownerAgentType,
|
||||
snapshot.workDir,
|
||||
new Date().toISOString(),
|
||||
chatJid,
|
||||
);
|
||||
}
|
||||
|
||||
function syncStoredRoomRegistrationSnapshotForJid(chatJid: string): void {
|
||||
const snapshot = collectRoomRegistrationSnapshot(db, chatJid);
|
||||
if (!snapshot) return;
|
||||
|
||||
const existing = getStoredRoomModeRow(chatJid);
|
||||
if (!existing) {
|
||||
insertStoredRoomSettings(
|
||||
db,
|
||||
chatJid,
|
||||
inferRoomModeFromRegisteredAgentTypes(getRegisteredAgentTypesForJid(chatJid)),
|
||||
'inferred',
|
||||
snapshot,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStoredRoomMetadata(db, chatJid, snapshot);
|
||||
}
|
||||
|
||||
function upsertStoredRoomMode(
|
||||
@@ -2017,12 +2313,16 @@ function upsertStoredRoomMode(
|
||||
source: RoomModeSource,
|
||||
): void {
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO room_settings (
|
||||
`INSERT INTO room_settings (
|
||||
chat_jid,
|
||||
room_mode,
|
||||
mode_source,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(chat_jid) DO UPDATE SET
|
||||
room_mode = excluded.room_mode,
|
||||
mode_source = excluded.mode_source,
|
||||
updated_at = excluded.updated_at`,
|
||||
).run(chatJid, roomMode, source, new Date().toISOString());
|
||||
}
|
||||
|
||||
@@ -2075,36 +2375,34 @@ export function getEffectiveRuntimeRoomMode(chatJid: string): RoomMode {
|
||||
: 'single';
|
||||
}
|
||||
|
||||
function backfillStoredRoomModes(database: Database): void {
|
||||
function backfillStoredRoomSettings(database: Database): void {
|
||||
const rows = database
|
||||
.prepare(
|
||||
`SELECT DISTINCT rg.jid
|
||||
FROM registered_groups rg
|
||||
LEFT JOIN room_settings rs ON rs.chat_jid = rg.jid
|
||||
WHERE rs.chat_jid IS NULL`,
|
||||
)
|
||||
.prepare(`SELECT DISTINCT jid FROM registered_groups`)
|
||||
.all() as Array<{ jid: string }>;
|
||||
if (rows.length === 0) return;
|
||||
|
||||
const insertRoomMode = database.prepare(
|
||||
`INSERT OR REPLACE INTO room_settings (
|
||||
chat_jid,
|
||||
room_mode,
|
||||
mode_source,
|
||||
updated_at
|
||||
) VALUES (?, ?, 'inferred', ?)`,
|
||||
);
|
||||
const tx = database.transaction((registeredRows: Array<{ jid: string }>) => {
|
||||
for (const row of registeredRows) {
|
||||
const snapshot = collectRoomRegistrationSnapshot(database, row.jid);
|
||||
if (!snapshot) continue;
|
||||
|
||||
const tx = database.transaction((missingRows: Array<{ jid: string }>) => {
|
||||
const updatedAt = new Date().toISOString();
|
||||
for (const row of missingRows) {
|
||||
const roomMode = inferRoomModeFromRegisteredAgentTypes(
|
||||
collectRegisteredAgentTypes(database, row.jid),
|
||||
const existing = getStoredRoomSettingsRowFromDatabase(database, row.jid);
|
||||
if (existing) {
|
||||
updateStoredRoomMetadata(database, row.jid, snapshot);
|
||||
continue;
|
||||
}
|
||||
|
||||
insertStoredRoomSettings(
|
||||
database,
|
||||
row.jid,
|
||||
inferRoomModeFromRegisteredAgentTypes(
|
||||
collectRegisteredAgentTypes(database, row.jid),
|
||||
),
|
||||
'inferred',
|
||||
snapshot,
|
||||
);
|
||||
insertRoomMode.run(row.jid, roomMode, updatedAt);
|
||||
}
|
||||
});
|
||||
|
||||
tx(rows);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,15 @@ beforeEach(() => {
|
||||
describe('service-routing global failover', () => {
|
||||
it('uses codex-review as owner during global failover', () => {
|
||||
setRegisteredGroup('dc:paired', {
|
||||
name: 'Paired Room Claude',
|
||||
folder: 'paired-claude',
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
setRegisteredGroup('dc:paired', {
|
||||
name: 'Paired Room Codex',
|
||||
folder: 'paired-codex',
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@Codex',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
@@ -57,15 +57,15 @@ describe('service-routing global failover', () => {
|
||||
|
||||
it('restores default lease after global failover is cleared', () => {
|
||||
setRegisteredGroup('dc:paired', {
|
||||
name: 'Paired Room Claude',
|
||||
folder: 'paired-claude',
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
setRegisteredGroup('dc:paired', {
|
||||
name: 'Paired Room Codex',
|
||||
folder: 'paired-codex',
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@Codex',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
@@ -85,15 +85,15 @@ describe('service-routing global failover', () => {
|
||||
|
||||
it('uses explicit single room mode to suppress reviewer lease on dual registration', () => {
|
||||
setRegisteredGroup('dc:explicit-single', {
|
||||
name: 'Explicit Single Claude',
|
||||
folder: 'explicit-single-claude',
|
||||
name: 'Explicit Single',
|
||||
folder: 'explicit-single',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
});
|
||||
setRegisteredGroup('dc:explicit-single', {
|
||||
name: 'Explicit Single Codex',
|
||||
folder: 'explicit-single-codex',
|
||||
name: 'Explicit Single',
|
||||
folder: 'explicit-single',
|
||||
trigger: '@Codex',
|
||||
added_at: '2024-01-01T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
|
||||
Reference in New Issue
Block a user