Merge branch 'codex/room-settings-ssot'
This commit is contained in:
174
src/db.test.ts
174
src/db.test.ts
@@ -36,6 +36,7 @@ import {
|
|||||||
getPairedWorkspace,
|
getPairedWorkspace,
|
||||||
getRouterStateForService,
|
getRouterStateForService,
|
||||||
getSession,
|
getSession,
|
||||||
|
getStoredRoomSettings,
|
||||||
getTaskById,
|
getTaskById,
|
||||||
listPairedWorkspacesForTask,
|
listPairedWorkspacesForTask,
|
||||||
markWorkItemDelivered,
|
markWorkItemDelivered,
|
||||||
@@ -46,6 +47,7 @@ import {
|
|||||||
setExplicitRoomMode,
|
setExplicitRoomMode,
|
||||||
storeChatMetadata,
|
storeChatMetadata,
|
||||||
storeMessage,
|
storeMessage,
|
||||||
|
updateRegisteredGroupName,
|
||||||
updatePairedTask,
|
updatePairedTask,
|
||||||
upsertPairedProject,
|
upsertPairedProject,
|
||||||
upsertPairedWorkspace,
|
upsertPairedWorkspace,
|
||||||
@@ -748,16 +750,16 @@ describe('registered group isMain', () => {
|
|||||||
|
|
||||||
it('filters duplicate jid registrations by agent type', () => {
|
it('filters duplicate jid registrations by agent type', () => {
|
||||||
setRegisteredGroup('dc:shared', {
|
setRegisteredGroup('dc:shared', {
|
||||||
name: 'Shared Room Claude',
|
name: 'Shared Room',
|
||||||
folder: 'shared-room',
|
folder: 'shared-room',
|
||||||
trigger: '@Andy',
|
trigger: '@Andy',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'claude-code',
|
agentType: 'claude-code',
|
||||||
});
|
});
|
||||||
setRegisteredGroup('dc:shared', {
|
setRegisteredGroup('dc:shared', {
|
||||||
name: 'Shared Room Codex',
|
name: 'Shared Room',
|
||||||
folder: 'shared-room',
|
folder: 'shared-room',
|
||||||
trigger: '@Andy',
|
trigger: '@Codex',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
});
|
});
|
||||||
@@ -766,24 +768,24 @@ describe('registered group isMain', () => {
|
|||||||
const codexGroups = getAllRegisteredGroups('codex');
|
const codexGroups = getAllRegisteredGroups('codex');
|
||||||
|
|
||||||
expect(claudeGroups['dc:shared']?.agentType).toBe('claude-code');
|
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']?.agentType).toBe('codex');
|
||||||
expect(codexGroups['dc:shared']?.name).toBe('Shared Room Codex');
|
expect(codexGroups['dc:shared']?.name).toBe('Shared Room');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('paired room registration', () => {
|
describe('paired room registration', () => {
|
||||||
it('detects when both Claude and Codex are registered on the same jid', () => {
|
it('detects when both Claude and Codex are registered on the same jid', () => {
|
||||||
setRegisteredGroup('dc:123', {
|
setRegisteredGroup('dc:123', {
|
||||||
name: 'Paired Room Claude',
|
name: 'Paired Room',
|
||||||
folder: 'paired-claude',
|
folder: 'paired-room',
|
||||||
trigger: '@Andy',
|
trigger: '@Andy',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'claude-code',
|
agentType: 'claude-code',
|
||||||
});
|
});
|
||||||
setRegisteredGroup('dc:123', {
|
setRegisteredGroup('dc:123', {
|
||||||
name: 'Paired Room Codex',
|
name: 'Paired Room',
|
||||||
folder: 'paired-codex',
|
folder: 'paired-room',
|
||||||
trigger: '@Codex',
|
trigger: '@Codex',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
@@ -813,15 +815,15 @@ describe('paired room registration', () => {
|
|||||||
|
|
||||||
it('keeps inferred room mode available when no explicit override exists', () => {
|
it('keeps inferred room mode available when no explicit override exists', () => {
|
||||||
setRegisteredGroup('dc:legacy-paired', {
|
setRegisteredGroup('dc:legacy-paired', {
|
||||||
name: 'Legacy Paired Claude',
|
name: 'Legacy Paired',
|
||||||
folder: 'legacy-paired-claude',
|
folder: 'legacy-paired',
|
||||||
trigger: '@Andy',
|
trigger: '@Andy',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'claude-code',
|
agentType: 'claude-code',
|
||||||
});
|
});
|
||||||
setRegisteredGroup('dc:legacy-paired', {
|
setRegisteredGroup('dc:legacy-paired', {
|
||||||
name: 'Legacy Paired Codex',
|
name: 'Legacy Paired',
|
||||||
folder: 'legacy-paired-codex',
|
folder: 'legacy-paired',
|
||||||
trigger: '@Codex',
|
trigger: '@Codex',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
@@ -876,16 +878,16 @@ describe('paired room registration', () => {
|
|||||||
);
|
);
|
||||||
insertGroup.run(
|
insertGroup.run(
|
||||||
'dc:legacy-sql',
|
'dc:legacy-sql',
|
||||||
'Legacy SQL Claude',
|
'Legacy SQL Room',
|
||||||
'legacy-sql-claude',
|
'legacy-sql-room',
|
||||||
'@Andy',
|
'@Claude',
|
||||||
'2024-01-01T00:00:00.000Z',
|
'2024-01-01T00:00:00.000Z',
|
||||||
'claude-code',
|
'claude-code',
|
||||||
);
|
);
|
||||||
insertGroup.run(
|
insertGroup.run(
|
||||||
'dc:legacy-sql',
|
'dc:legacy-sql',
|
||||||
'Legacy SQL Codex',
|
'Legacy SQL Room',
|
||||||
'legacy-sql-codex',
|
'legacy-sql-room',
|
||||||
'@Codex',
|
'@Codex',
|
||||||
'2024-01-01T00:00:00.000Z',
|
'2024-01-01T00:00:00.000Z',
|
||||||
'codex',
|
'codex',
|
||||||
@@ -897,19 +899,147 @@ describe('paired room registration', () => {
|
|||||||
expect(getExplicitRoomMode('dc:legacy-sql')).toBeUndefined();
|
expect(getExplicitRoomMode('dc:legacy-sql')).toBeUndefined();
|
||||||
expect(getEffectiveRoomMode('dc:legacy-sql')).toBe('tribunal');
|
expect(getEffectiveRoomMode('dc:legacy-sql')).toBe('tribunal');
|
||||||
expect(getEffectiveRuntimeRoomMode('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', () => {
|
it('lets explicit single override dual registration for paired-room checks', () => {
|
||||||
setRegisteredGroup('dc:explicit-single', {
|
setRegisteredGroup('dc:explicit-single', {
|
||||||
name: 'Explicit Single Claude',
|
name: 'Explicit Single',
|
||||||
folder: 'explicit-single-claude',
|
folder: 'explicit-single',
|
||||||
trigger: '@Andy',
|
trigger: '@Andy',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'claude-code',
|
agentType: 'claude-code',
|
||||||
});
|
});
|
||||||
setRegisteredGroup('dc:explicit-single', {
|
setRegisteredGroup('dc:explicit-single', {
|
||||||
name: 'Explicit Single Codex',
|
name: 'Explicit Single',
|
||||||
folder: 'explicit-single-codex',
|
folder: 'explicit-single',
|
||||||
trigger: '@Codex',
|
trigger: '@Codex',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
|
|||||||
414
src/db.ts
414
src/db.ts
@@ -9,6 +9,7 @@ import {
|
|||||||
CODEX_REVIEW_SERVICE_ID,
|
CODEX_REVIEW_SERVICE_ID,
|
||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
normalizeServiceId,
|
normalizeServiceId,
|
||||||
|
OWNER_AGENT_TYPE,
|
||||||
REVIEWER_AGENT_TYPE,
|
REVIEWER_AGENT_TYPE,
|
||||||
SERVICE_ID,
|
SERVICE_ID,
|
||||||
SERVICE_SESSION_SCOPE,
|
SERVICE_SESSION_SCOPE,
|
||||||
@@ -45,6 +46,29 @@ interface StoredRoomModeRow {
|
|||||||
source: RoomModeSource;
|
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 {
|
export interface WorkItem {
|
||||||
id: number;
|
id: number;
|
||||||
group_folder: string;
|
group_folder: string;
|
||||||
@@ -310,8 +334,16 @@ function createSchema(database: Database): void {
|
|||||||
chat_jid TEXT PRIMARY KEY,
|
chat_jid TEXT PRIMARY KEY,
|
||||||
room_mode TEXT NOT NULL,
|
room_mode TEXT NOT NULL,
|
||||||
mode_source TEXT NOT NULL DEFAULT 'explicit',
|
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,
|
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 (
|
CREATE TABLE IF NOT EXISTS service_handoffs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -402,6 +434,52 @@ function createSchema(database: Database): void {
|
|||||||
/* column already exists */
|
/* 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(
|
database.exec(
|
||||||
`UPDATE room_settings
|
`UPDATE room_settings
|
||||||
SET mode_source = 'explicit'
|
SET mode_source = 'explicit'
|
||||||
@@ -807,7 +885,7 @@ function createSchema(database: Database): void {
|
|||||||
/* columns already exist */
|
/* columns already exist */
|
||||||
}
|
}
|
||||||
|
|
||||||
backfillStoredRoomModes(database);
|
backfillStoredRoomSettings(database);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initDatabase(): void {
|
export function initDatabase(): void {
|
||||||
@@ -835,6 +913,22 @@ export function _initTestDatabaseFromFile(dbPath: string): void {
|
|||||||
createSchema(db);
|
createSchema(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal - for tests only. */
|
||||||
|
export function _setStoredRoomOwnerAgentTypeForTests(
|
||||||
|
chatJid: string,
|
||||||
|
ownerAgentType: AgentType | null,
|
||||||
|
): void {
|
||||||
|
if (!db) {
|
||||||
|
throw new Error('Database not initialized');
|
||||||
|
}
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE room_settings
|
||||||
|
SET owner_agent_type = ?,
|
||||||
|
updated_at = ?
|
||||||
|
WHERE chat_jid = ?`,
|
||||||
|
).run(ownerAgentType, new Date().toISOString(), chatJid);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store chat metadata only (no message content).
|
* Store chat metadata only (no message content).
|
||||||
* Used for all chats to enable group discovery without storing sensitive content.
|
* Used for all chats to enable group discovery without storing sensitive content.
|
||||||
@@ -1859,25 +1953,29 @@ export function setRegisteredGroup(jid: string, group: RegisteredGroup): void {
|
|||||||
if (!isValidGroupFolder(group.folder)) {
|
if (!isValidGroupFolder(group.folder)) {
|
||||||
throw new Error(`Invalid group folder "${group.folder}" for JID ${jid}`);
|
throw new Error(`Invalid group folder "${group.folder}" for JID ${jid}`);
|
||||||
}
|
}
|
||||||
db.prepare(
|
const tx = db.transaction(() => {
|
||||||
`INSERT OR REPLACE INTO registered_groups (jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main, agent_type, work_dir)
|
db.prepare(
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO registered_groups (jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main, agent_type, work_dir)
|
||||||
).run(
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
jid,
|
).run(
|
||||||
group.name,
|
jid,
|
||||||
group.folder,
|
group.name,
|
||||||
group.trigger,
|
group.folder,
|
||||||
group.added_at,
|
group.trigger,
|
||||||
group.agentConfig ? JSON.stringify(group.agentConfig) : null,
|
group.added_at,
|
||||||
group.requiresTrigger === undefined ? 1 : group.requiresTrigger ? 1 : 0,
|
group.agentConfig ? JSON.stringify(group.agentConfig) : null,
|
||||||
group.isMain ? 1 : 0,
|
group.requiresTrigger === undefined ? 1 : group.requiresTrigger ? 1 : 0,
|
||||||
group.agentType || 'claude-code',
|
group.isMain ? 1 : 0,
|
||||||
group.workDir || null,
|
group.agentType || 'claude-code',
|
||||||
);
|
group.workDir || null,
|
||||||
|
);
|
||||||
|
|
||||||
if (!existingRoomMode || existingRoomMode.source === 'inferred') {
|
syncStoredRoomRegistrationSnapshotForJid(jid);
|
||||||
syncInferredRoomModeForJid(jid);
|
if (!existingRoomMode || existingRoomMode.source === 'inferred') {
|
||||||
}
|
syncInferredRoomModeForJid(jid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateRegisteredGroupName(jid: string, name: string): void {
|
export function updateRegisteredGroupName(jid: string, name: string): void {
|
||||||
@@ -1885,6 +1983,7 @@ export function updateRegisteredGroupName(jid: string, name: string): void {
|
|||||||
name,
|
name,
|
||||||
jid,
|
jid,
|
||||||
);
|
);
|
||||||
|
syncStoredRoomRegistrationSnapshotForJid(jid);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllRegisteredGroups(
|
export function getAllRegisteredGroups(
|
||||||
@@ -1974,6 +2073,15 @@ export function inferRoomModeFromRegisteredAgentTypes(
|
|||||||
return types.has('claude-code') && types.has('codex') ? 'tribunal' : 'single';
|
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.
|
* Internal registration/backfill helper.
|
||||||
* This infers the stored room mode from current registrations and must not be
|
* This infers the stored room mode from current registrations and must not be
|
||||||
@@ -1991,24 +2099,228 @@ function normalizeRoomModeSource(
|
|||||||
return source === 'explicit' || source === 'inferred' ? source : undefined;
|
return source === 'explicit' || source === 'inferred' ? source : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredRoomModeRow(chatJid: string): StoredRoomModeRow | undefined {
|
function normalizeStoredAgentType(
|
||||||
if (!db) return undefined;
|
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(
|
.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
|
FROM room_settings
|
||||||
WHERE chat_jid = ?`,
|
WHERE chat_jid = ?`,
|
||||||
)
|
)
|
||||||
.get(chatJid) as
|
.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;
|
| undefined;
|
||||||
const roomMode =
|
const roomMode =
|
||||||
row?.room_mode === 'single' || row?.room_mode === 'tribunal'
|
row?.room_mode === 'single' || row?.room_mode === 'tribunal'
|
||||||
? row.room_mode
|
? row.room_mode
|
||||||
: undefined;
|
: undefined;
|
||||||
const source = normalizeRoomModeSource(row?.mode_source);
|
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(
|
function upsertStoredRoomMode(
|
||||||
@@ -2017,12 +2329,16 @@ function upsertStoredRoomMode(
|
|||||||
source: RoomModeSource,
|
source: RoomModeSource,
|
||||||
): void {
|
): void {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT OR REPLACE INTO room_settings (
|
`INSERT INTO room_settings (
|
||||||
chat_jid,
|
chat_jid,
|
||||||
room_mode,
|
room_mode,
|
||||||
mode_source,
|
mode_source,
|
||||||
updated_at
|
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());
|
).run(chatJid, roomMode, source, new Date().toISOString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2075,36 +2391,34 @@ export function getEffectiveRuntimeRoomMode(chatJid: string): RoomMode {
|
|||||||
: 'single';
|
: 'single';
|
||||||
}
|
}
|
||||||
|
|
||||||
function backfillStoredRoomModes(database: Database): void {
|
function backfillStoredRoomSettings(database: Database): void {
|
||||||
const rows = database
|
const rows = database
|
||||||
.prepare(
|
.prepare(`SELECT DISTINCT jid FROM registered_groups`)
|
||||||
`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`,
|
|
||||||
)
|
|
||||||
.all() as Array<{ jid: string }>;
|
.all() as Array<{ jid: string }>;
|
||||||
if (rows.length === 0) return;
|
if (rows.length === 0) return;
|
||||||
|
|
||||||
const insertRoomMode = database.prepare(
|
const tx = database.transaction((registeredRows: Array<{ jid: string }>) => {
|
||||||
`INSERT OR REPLACE INTO room_settings (
|
for (const row of registeredRows) {
|
||||||
chat_jid,
|
const snapshot = collectRoomRegistrationSnapshot(database, row.jid);
|
||||||
room_mode,
|
if (!snapshot) continue;
|
||||||
mode_source,
|
|
||||||
updated_at
|
|
||||||
) VALUES (?, ?, 'inferred', ?)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const tx = database.transaction((missingRows: Array<{ jid: string }>) => {
|
const existing = getStoredRoomSettingsRowFromDatabase(database, row.jid);
|
||||||
const updatedAt = new Date().toISOString();
|
if (existing) {
|
||||||
for (const row of missingRows) {
|
updateStoredRoomMetadata(database, row.jid, snapshot);
|
||||||
const roomMode = inferRoomModeFromRegisteredAgentTypes(
|
continue;
|
||||||
collectRegisteredAgentTypes(database, row.jid),
|
}
|
||||||
|
|
||||||
|
insertStoredRoomSettings(
|
||||||
|
database,
|
||||||
|
row.jid,
|
||||||
|
inferRoomModeFromRegisteredAgentTypes(
|
||||||
|
collectRegisteredAgentTypes(database, row.jid),
|
||||||
|
),
|
||||||
|
'inferred',
|
||||||
|
snapshot,
|
||||||
);
|
);
|
||||||
insertRoomMode.run(row.jid, roomMode, updatedAt);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tx(rows);
|
tx(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
_setStoredRoomOwnerAgentTypeForTests,
|
||||||
_initTestDatabase,
|
_initTestDatabase,
|
||||||
setExplicitRoomMode,
|
setExplicitRoomMode,
|
||||||
setRegisteredGroup,
|
setRegisteredGroup,
|
||||||
@@ -22,15 +23,15 @@ beforeEach(() => {
|
|||||||
describe('service-routing global failover', () => {
|
describe('service-routing global failover', () => {
|
||||||
it('uses codex-review as owner during global failover', () => {
|
it('uses codex-review as owner during global failover', () => {
|
||||||
setRegisteredGroup('dc:paired', {
|
setRegisteredGroup('dc:paired', {
|
||||||
name: 'Paired Room Claude',
|
name: 'Paired Room',
|
||||||
folder: 'paired-claude',
|
folder: 'paired-room',
|
||||||
trigger: '@Andy',
|
trigger: '@Andy',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'claude-code',
|
agentType: 'claude-code',
|
||||||
});
|
});
|
||||||
setRegisteredGroup('dc:paired', {
|
setRegisteredGroup('dc:paired', {
|
||||||
name: 'Paired Room Codex',
|
name: 'Paired Room',
|
||||||
folder: 'paired-codex',
|
folder: 'paired-room',
|
||||||
trigger: '@Codex',
|
trigger: '@Codex',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
@@ -57,15 +58,15 @@ describe('service-routing global failover', () => {
|
|||||||
|
|
||||||
it('restores default lease after global failover is cleared', () => {
|
it('restores default lease after global failover is cleared', () => {
|
||||||
setRegisteredGroup('dc:paired', {
|
setRegisteredGroup('dc:paired', {
|
||||||
name: 'Paired Room Claude',
|
name: 'Paired Room',
|
||||||
folder: 'paired-claude',
|
folder: 'paired-room',
|
||||||
trigger: '@Andy',
|
trigger: '@Andy',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'claude-code',
|
agentType: 'claude-code',
|
||||||
});
|
});
|
||||||
setRegisteredGroup('dc:paired', {
|
setRegisteredGroup('dc:paired', {
|
||||||
name: 'Paired Room Codex',
|
name: 'Paired Room',
|
||||||
folder: 'paired-codex',
|
folder: 'paired-room',
|
||||||
trigger: '@Codex',
|
trigger: '@Codex',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
@@ -85,15 +86,15 @@ describe('service-routing global failover', () => {
|
|||||||
|
|
||||||
it('uses explicit single room mode to suppress reviewer lease on dual registration', () => {
|
it('uses explicit single room mode to suppress reviewer lease on dual registration', () => {
|
||||||
setRegisteredGroup('dc:explicit-single', {
|
setRegisteredGroup('dc:explicit-single', {
|
||||||
name: 'Explicit Single Claude',
|
name: 'Explicit Single',
|
||||||
folder: 'explicit-single-claude',
|
folder: 'explicit-single',
|
||||||
trigger: '@Andy',
|
trigger: '@Andy',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'claude-code',
|
agentType: 'claude-code',
|
||||||
});
|
});
|
||||||
setRegisteredGroup('dc:explicit-single', {
|
setRegisteredGroup('dc:explicit-single', {
|
||||||
name: 'Explicit Single Codex',
|
name: 'Explicit Single',
|
||||||
folder: 'explicit-single-codex',
|
folder: 'explicit-single',
|
||||||
trigger: '@Codex',
|
trigger: '@Codex',
|
||||||
added_at: '2024-01-01T00:00:00.000Z',
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
agentType: 'codex',
|
agentType: 'codex',
|
||||||
@@ -108,6 +109,53 @@ describe('service-routing global failover', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses stored owner agent type when room_settings selects a different owner', () => {
|
||||||
|
setRegisteredGroup('dc:stored-owner-claude', {
|
||||||
|
name: 'Stored Owner Claude',
|
||||||
|
folder: 'stored-owner-claude',
|
||||||
|
trigger: '@Andy',
|
||||||
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
});
|
||||||
|
setRegisteredGroup('dc:stored-owner-claude', {
|
||||||
|
name: 'Stored Owner Claude',
|
||||||
|
folder: 'stored-owner-claude',
|
||||||
|
trigger: '@Codex',
|
||||||
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
|
agentType: 'codex',
|
||||||
|
});
|
||||||
|
setExplicitRoomMode('dc:stored-owner-claude', 'single');
|
||||||
|
_setStoredRoomOwnerAgentTypeForTests(
|
||||||
|
'dc:stored-owner-claude',
|
||||||
|
'claude-code',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getEffectiveChannelLease('dc:stored-owner-claude')).toMatchObject({
|
||||||
|
chat_jid: 'dc:stored-owner-claude',
|
||||||
|
owner_service_id: 'claude',
|
||||||
|
reviewer_service_id: null,
|
||||||
|
explicit: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the available service when stored owner agent type is unavailable', () => {
|
||||||
|
setRegisteredGroup('dc:stored-owner-fallback', {
|
||||||
|
name: 'Stored Owner Fallback',
|
||||||
|
folder: 'stored-owner-fallback',
|
||||||
|
trigger: '@Andy',
|
||||||
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
|
agentType: 'claude-code',
|
||||||
|
});
|
||||||
|
_setStoredRoomOwnerAgentTypeForTests('dc:stored-owner-fallback', 'codex');
|
||||||
|
|
||||||
|
expect(getEffectiveChannelLease('dc:stored-owner-fallback')).toMatchObject({
|
||||||
|
chat_jid: 'dc:stored-owner-fallback',
|
||||||
|
owner_service_id: 'claude',
|
||||||
|
reviewer_service_id: null,
|
||||||
|
explicit: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('creates a same-service reviewer lease when explicit tribunal is runnable on a solo registration', () => {
|
it('creates a same-service reviewer lease when explicit tribunal is runnable on a solo registration', () => {
|
||||||
setRegisteredGroup('dc:explicit-tribunal', {
|
setRegisteredGroup('dc:explicit-tribunal', {
|
||||||
name: 'Explicit Tribunal Claude',
|
name: 'Explicit Tribunal Claude',
|
||||||
@@ -145,4 +193,13 @@ describe('service-routing global failover', () => {
|
|||||||
explicit: false,
|
explicit: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the legacy claude fallback for chats without room settings', () => {
|
||||||
|
expect(getEffectiveChannelLease('dc:unregistered')).toMatchObject({
|
||||||
|
chat_jid: 'dc:unregistered',
|
||||||
|
owner_service_id: 'claude',
|
||||||
|
reviewer_service_id: null,
|
||||||
|
explicit: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import {
|
|||||||
getAllChannelOwnerLeases,
|
getAllChannelOwnerLeases,
|
||||||
getEffectiveRuntimeRoomMode,
|
getEffectiveRuntimeRoomMode,
|
||||||
getRegisteredAgentTypesForJid,
|
getRegisteredAgentTypesForJid,
|
||||||
|
getStoredRoomSettings,
|
||||||
setChannelOwnerLease,
|
setChannelOwnerLease,
|
||||||
type ChannelOwnerLeaseRow,
|
type ChannelOwnerLeaseRow,
|
||||||
} from './db.js';
|
} from './db.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
import type { AgentType } from './types.js';
|
||||||
|
|
||||||
export interface EffectiveChannelLease {
|
export interface EffectiveChannelLease {
|
||||||
chat_jid: string;
|
chat_jid: string;
|
||||||
@@ -55,21 +57,44 @@ function normalizeLeaseRow(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultLease(chatJid: string): EffectiveChannelLease {
|
function getServiceIdForAgentType(agentType: AgentType): string {
|
||||||
|
return agentType === 'codex' ? CODEX_MAIN_SERVICE_ID : CLAUDE_SERVICE_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferFallbackOwnerAgentType(
|
||||||
|
hasClaude: boolean,
|
||||||
|
hasCodex: boolean,
|
||||||
|
): AgentType | undefined {
|
||||||
|
if (hasClaude && hasCodex) return OWNER_AGENT_TYPE;
|
||||||
|
if (hasCodex) return 'codex';
|
||||||
|
if (hasClaude) return 'claude-code';
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDefaultOwnerAgentType(chatJid: string): AgentType | undefined {
|
||||||
const types = getRegisteredAgentTypesForJid(chatJid);
|
const types = getRegisteredAgentTypesForJid(chatJid);
|
||||||
const hasClaude = types.includes('claude-code');
|
const hasClaude = types.includes('claude-code');
|
||||||
const hasCodex = types.includes('codex');
|
const hasCodex = types.includes('codex');
|
||||||
|
const storedOwnerAgentType = getStoredRoomSettings(chatJid)?.ownerAgentType;
|
||||||
|
|
||||||
|
if (storedOwnerAgentType === 'claude-code' && hasClaude) {
|
||||||
|
return 'claude-code';
|
||||||
|
}
|
||||||
|
if (storedOwnerAgentType === 'codex' && hasCodex) {
|
||||||
|
return 'codex';
|
||||||
|
}
|
||||||
|
|
||||||
|
return inferFallbackOwnerAgentType(hasClaude, hasCodex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultLease(chatJid: string): EffectiveChannelLease {
|
||||||
const roomMode = getEffectiveRuntimeRoomMode(chatJid);
|
const roomMode = getEffectiveRuntimeRoomMode(chatJid);
|
||||||
|
const ownerAgentType = resolveDefaultOwnerAgentType(chatJid);
|
||||||
|
const ownerServiceId = ownerAgentType
|
||||||
|
? getServiceIdForAgentType(ownerAgentType)
|
||||||
|
: CLAUDE_SERVICE_ID;
|
||||||
|
|
||||||
if (roomMode === 'tribunal') {
|
if (roomMode === 'tribunal') {
|
||||||
const ownerServiceId =
|
|
||||||
hasClaude && hasCodex
|
|
||||||
? OWNER_AGENT_TYPE === 'codex'
|
|
||||||
? CODEX_MAIN_SERVICE_ID
|
|
||||||
: CLAUDE_SERVICE_ID
|
|
||||||
: hasCodex
|
|
||||||
? CODEX_MAIN_SERVICE_ID
|
|
||||||
: CLAUDE_SERVICE_ID;
|
|
||||||
return {
|
return {
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
owner_service_id: ownerServiceId,
|
owner_service_id: ownerServiceId,
|
||||||
@@ -81,33 +106,9 @@ function getDefaultLease(chatJid: string): EffectiveChannelLease {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCodex) {
|
|
||||||
return {
|
|
||||||
chat_jid: chatJid,
|
|
||||||
owner_service_id: CODEX_MAIN_SERVICE_ID,
|
|
||||||
reviewer_service_id: null,
|
|
||||||
arbiter_service_id: null,
|
|
||||||
activated_at: null,
|
|
||||||
reason: null,
|
|
||||||
explicit: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasClaude) {
|
|
||||||
return {
|
|
||||||
chat_jid: chatJid,
|
|
||||||
owner_service_id: CLAUDE_SERVICE_ID,
|
|
||||||
reviewer_service_id: null,
|
|
||||||
arbiter_service_id: null,
|
|
||||||
activated_at: null,
|
|
||||||
reason: null,
|
|
||||||
explicit: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
owner_service_id: CLAUDE_SERVICE_ID,
|
owner_service_id: ownerServiceId,
|
||||||
reviewer_service_id: null,
|
reviewer_service_id: null,
|
||||||
arbiter_service_id: null,
|
arbiter_service_id: null,
|
||||||
activated_at: null,
|
activated_at: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user