feat: support room-level reviewer and arbiter agent selection
This commit is contained in:
@@ -798,6 +798,14 @@ If folder is omitted, the host generates one automatically.`,
|
||||
.enum(['claude-code', 'codex'])
|
||||
.optional()
|
||||
.describe('Preferred owner agent type for the room'),
|
||||
reviewer_agent_type: z
|
||||
.enum(['claude-code', 'codex'])
|
||||
.optional()
|
||||
.describe('Optional reviewer agent type override for tribunal rooms'),
|
||||
arbiter_agent_type: z
|
||||
.enum(['claude-code', 'codex'])
|
||||
.optional()
|
||||
.describe('Optional arbiter agent type override for tribunal rooms'),
|
||||
folder: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -826,6 +834,8 @@ If folder is omitted, the host generates one automatically.`,
|
||||
name: string;
|
||||
room_mode?: 'single' | 'tribunal';
|
||||
owner_agent_type?: 'claude-code' | 'codex';
|
||||
reviewer_agent_type?: 'claude-code' | 'codex';
|
||||
arbiter_agent_type?: 'claude-code' | 'codex';
|
||||
folder?: string;
|
||||
trigger?: string;
|
||||
requires_trigger?: boolean;
|
||||
@@ -850,6 +860,8 @@ If folder is omitted, the host generates one automatically.`,
|
||||
name: args.name,
|
||||
room_mode: args.room_mode || 'single',
|
||||
owner_agent_type: args.owner_agent_type,
|
||||
reviewer_agent_type: args.reviewer_agent_type,
|
||||
arbiter_agent_type: args.arbiter_agent_type,
|
||||
folder: args.folder,
|
||||
trigger: args.trigger,
|
||||
requiresTrigger: args.requires_trigger,
|
||||
|
||||
@@ -2396,6 +2396,29 @@ describe('room assignment writes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('includes arbiter-only room agent overrides in capability views', () => {
|
||||
assignRoom('dc:arbiter-only-capability', {
|
||||
name: 'Arbiter Only Capability',
|
||||
roomMode: 'tribunal',
|
||||
ownerAgentType: 'claude-code',
|
||||
reviewerAgentType: 'claude-code',
|
||||
arbiterAgentType: 'codex',
|
||||
folder: 'arbiter-only-capability',
|
||||
});
|
||||
|
||||
expect(getRegisteredAgentTypesForJid('dc:arbiter-only-capability').sort()).toEqual([
|
||||
'claude-code',
|
||||
'codex',
|
||||
]);
|
||||
expect(
|
||||
getAllRoomBindings('codex')['dc:arbiter-only-capability'],
|
||||
).toMatchObject({
|
||||
name: 'Arbiter Only Capability',
|
||||
folder: 'arbiter-only-capability',
|
||||
agentType: 'codex',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates room_settings-backed metadata across tribunal capability views', () => {
|
||||
assignRoom('dc:projection-room', {
|
||||
name: 'Projection Room',
|
||||
|
||||
@@ -57,6 +57,7 @@ export {
|
||||
getRouterStateForService,
|
||||
getSession,
|
||||
getSessionForAgentType,
|
||||
getStoredRoomRoleAgentPlan,
|
||||
getStoredRoomSettings,
|
||||
getTaskById,
|
||||
getTasksForGroup,
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Database } from 'bun:sqlite';
|
||||
|
||||
import { OWNER_AGENT_TYPE, REVIEWER_AGENT_TYPE } from '../config.js';
|
||||
import {
|
||||
ARBITER_AGENT_TYPE,
|
||||
OWNER_AGENT_TYPE,
|
||||
REVIEWER_AGENT_TYPE,
|
||||
} from '../config.js';
|
||||
import { isValidGroupFolder } from '../group-folder.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { type RoleAgentPlan, resolveRoleAgentPlan } from '../role-agent-plan.js';
|
||||
import type { AgentType, RegisteredGroup, RoomMode } from '../types.js';
|
||||
|
||||
export type RoomModeSource = 'explicit' | 'inferred';
|
||||
@@ -58,8 +63,12 @@ export interface LegacyRoomMigrationPlan {
|
||||
export interface SyncRoomRoleOverridesOptions {
|
||||
ownerAgentConfig?: RegisteredGroup['agentConfig'];
|
||||
ownerCreatedAt?: string;
|
||||
reviewerAgentType?: AgentType;
|
||||
reviewerAgentConfig?: RegisteredGroup['agentConfig'];
|
||||
reviewerCreatedAt?: string;
|
||||
arbiterAgentType?: AgentType | null;
|
||||
arbiterAgentConfig?: RegisteredGroup['agentConfig'];
|
||||
arbiterCreatedAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
@@ -207,14 +216,32 @@ export function getStoredRoomRowsFromDatabase(
|
||||
.filter((row): row is StoredRoomSettings => Boolean(row));
|
||||
}
|
||||
|
||||
export function resolveStoredRoomRoleAgentPlan(
|
||||
database: Database,
|
||||
stored: StoredRoomSettings,
|
||||
): RoleAgentPlan {
|
||||
const overrides = getStoredRoomRoleOverrideRows(database, stored.chatJid);
|
||||
const ownerAgentType =
|
||||
stored.ownerAgentType ?? overrides.get('owner')?.agentType ?? OWNER_AGENT_TYPE;
|
||||
|
||||
return resolveRoleAgentPlan({
|
||||
paired: stored.roomMode === 'tribunal',
|
||||
groupAgentType: ownerAgentType,
|
||||
configuredReviewer:
|
||||
overrides.get('reviewer')?.agentType ?? REVIEWER_AGENT_TYPE,
|
||||
configuredArbiter: overrides.get('arbiter')?.agentType ?? ARBITER_AGENT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildRegisteredGroupFromStoredSettings(
|
||||
database: Database,
|
||||
stored: StoredRoomSettings,
|
||||
requestedAgentType?: AgentType,
|
||||
): (RegisteredGroup & { jid: string }) | undefined {
|
||||
const capabilityTypes = resolveStoredRoomCapabilityTypes(database, stored);
|
||||
const rolePlan = resolveStoredRoomRoleAgentPlan(database, stored);
|
||||
const ownerAgentType =
|
||||
stored.ownerAgentType ||
|
||||
rolePlan.ownerAgentType ||
|
||||
(capabilityTypes.length > 0
|
||||
? inferOwnerAgentTypeFromRegisteredAgentTypes(capabilityTypes)
|
||||
: undefined);
|
||||
@@ -233,8 +260,12 @@ export function buildRegisteredGroupFromStoredSettings(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const role: 'owner' | 'reviewer' =
|
||||
ownerAgentType === resolvedAgentType ? 'owner' : 'reviewer';
|
||||
const role: 'owner' | 'reviewer' | 'arbiter' =
|
||||
ownerAgentType === resolvedAgentType
|
||||
? 'owner'
|
||||
: rolePlan.reviewerAgentType === resolvedAgentType
|
||||
? 'reviewer'
|
||||
: 'arbiter';
|
||||
const capabilityMetadata = getStoredRoomRoleOverrideRows(
|
||||
database,
|
||||
stored.chatJid,
|
||||
@@ -273,24 +304,17 @@ export function resolveStoredRoomCapabilityTypes(
|
||||
database: Database,
|
||||
stored: StoredRoomSettings,
|
||||
): AgentType[] {
|
||||
const overrides = getStoredRoomRoleOverrideRows(database, stored.chatJid);
|
||||
const ownerAgentType =
|
||||
stored.ownerAgentType ??
|
||||
overrides.get('owner')?.agentType ??
|
||||
OWNER_AGENT_TYPE;
|
||||
|
||||
if (stored.modeSource !== 'explicit') {
|
||||
return inferStoredRoomCapabilityTypes(database, stored);
|
||||
}
|
||||
|
||||
const types = new Set<AgentType>([ownerAgentType]);
|
||||
if (stored.roomMode === 'tribunal') {
|
||||
const reviewerAgentType =
|
||||
overrides.get('reviewer')?.agentType ??
|
||||
(ownerAgentType === REVIEWER_AGENT_TYPE
|
||||
? OWNER_AGENT_TYPE
|
||||
: REVIEWER_AGENT_TYPE);
|
||||
types.add(reviewerAgentType);
|
||||
const rolePlan = resolveStoredRoomRoleAgentPlan(database, stored);
|
||||
const types = new Set<AgentType>([rolePlan.ownerAgentType]);
|
||||
if (stored.roomMode === 'tribunal' && rolePlan.reviewerAgentType) {
|
||||
types.add(rolePlan.reviewerAgentType);
|
||||
}
|
||||
if (stored.roomMode === 'tribunal' && rolePlan.arbiterAgentType) {
|
||||
types.add(rolePlan.arbiterAgentType);
|
||||
}
|
||||
return [...types];
|
||||
}
|
||||
@@ -526,11 +550,19 @@ export function syncRoomRoleOverridesForRoom(
|
||||
});
|
||||
|
||||
if (roomMode === 'tribunal') {
|
||||
const reviewerAgentType =
|
||||
ownerAgentType === REVIEWER_AGENT_TYPE
|
||||
const existingReviewerOverride = existingOverrides.get('reviewer');
|
||||
const previousDefaultReviewerAgentType =
|
||||
existingOwnerOverride?.agentType === REVIEWER_AGENT_TYPE
|
||||
? OWNER_AGENT_TYPE
|
||||
: REVIEWER_AGENT_TYPE;
|
||||
const existingReviewerOverride = existingOverrides.get('reviewer');
|
||||
const computedReviewerAgentType =
|
||||
ownerAgentType === REVIEWER_AGENT_TYPE ? OWNER_AGENT_TYPE : REVIEWER_AGENT_TYPE;
|
||||
const reviewerAgentType =
|
||||
options.reviewerAgentType ??
|
||||
(existingReviewerOverride &&
|
||||
existingReviewerOverride.agentType !== previousDefaultReviewerAgentType
|
||||
? existingReviewerOverride.agentType
|
||||
: computedReviewerAgentType);
|
||||
upsertRoomRoleOverride(database, chatJid, {
|
||||
role: 'reviewer',
|
||||
agentType: reviewerAgentType,
|
||||
@@ -547,6 +579,40 @@ export function syncRoomRoleOverridesForRoom(
|
||||
now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const existingArbiterOverride = existingOverrides.get('arbiter');
|
||||
const arbiterAgentType =
|
||||
options.arbiterAgentType ??
|
||||
existingArbiterOverride?.agentType ??
|
||||
ARBITER_AGENT_TYPE ??
|
||||
null;
|
||||
|
||||
if (arbiterAgentType) {
|
||||
upsertRoomRoleOverride(database, chatJid, {
|
||||
role: 'arbiter',
|
||||
agentType: arbiterAgentType,
|
||||
agentConfig:
|
||||
options.arbiterAgentConfig ??
|
||||
(existingArbiterOverride?.agentType === arbiterAgentType
|
||||
? existingArbiterOverride.agentConfig
|
||||
: undefined),
|
||||
createdAt:
|
||||
(existingArbiterOverride?.agentType === arbiterAgentType
|
||||
? existingArbiterOverride.createdAt
|
||||
: undefined) ??
|
||||
options.arbiterCreatedAt ??
|
||||
now,
|
||||
updatedAt: now,
|
||||
});
|
||||
} else {
|
||||
database
|
||||
.prepare(
|
||||
`DELETE FROM room_role_overrides
|
||||
WHERE chat_jid = ?
|
||||
AND role = 'arbiter'`,
|
||||
)
|
||||
.run(chatJid);
|
||||
}
|
||||
} else {
|
||||
database
|
||||
.prepare(
|
||||
@@ -555,6 +621,13 @@ export function syncRoomRoleOverridesForRoom(
|
||||
AND role = 'reviewer'`,
|
||||
)
|
||||
.run(chatJid);
|
||||
database
|
||||
.prepare(
|
||||
`DELETE FROM room_role_overrides
|
||||
WHERE chat_jid = ?
|
||||
AND role = 'arbiter'`,
|
||||
)
|
||||
.run(chatJid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
normalizeStoredAgentType,
|
||||
resolveAssignedRoomFolder,
|
||||
resolveStoredRoomCapabilityTypes,
|
||||
resolveStoredRoomRoleAgentPlan,
|
||||
syncRoomRoleOverridesForRoom,
|
||||
updateStoredRoomMetadata,
|
||||
upsertRoomRoleOverride,
|
||||
@@ -31,6 +32,8 @@ export interface AssignRoomInput {
|
||||
name: string;
|
||||
roomMode?: RoomMode;
|
||||
ownerAgentType?: AgentType;
|
||||
reviewerAgentType?: AgentType;
|
||||
arbiterAgentType?: AgentType | null;
|
||||
folder?: string;
|
||||
isMain?: boolean;
|
||||
workDir?: string;
|
||||
@@ -233,6 +236,8 @@ export function assignRoomInDatabase(
|
||||
syncRoomRoleOverridesForRoom(database, chatJid, roomMode, ownerAgentType, {
|
||||
ownerAgentConfig: input.ownerAgentConfig,
|
||||
ownerCreatedAt: input.addedAt ?? now,
|
||||
reviewerAgentType: input.reviewerAgentType,
|
||||
arbiterAgentType: input.arbiterAgentType,
|
||||
updatedAt: input.addedAt ?? now,
|
||||
});
|
||||
})();
|
||||
@@ -294,6 +299,14 @@ export function getStoredRoomSettingsFromDatabase(
|
||||
return getStoredRoomSettingsRowFromDatabase(database, chatJid);
|
||||
}
|
||||
|
||||
export function getStoredRoomRoleAgentPlanFromDatabase(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
): ReturnType<typeof resolveStoredRoomRoleAgentPlan> | undefined {
|
||||
const stored = getStoredRoomSettingsRowFromDatabase(database, chatJid);
|
||||
return stored ? resolveStoredRoomRoleAgentPlan(database, stored) : undefined;
|
||||
}
|
||||
|
||||
function getStoredRoomModeRowFromDatabase(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
getExplicitRoomModeFromDatabase,
|
||||
getRegisteredAgentTypesForJidFromDatabase,
|
||||
getRegisteredGroupFromDatabase,
|
||||
getStoredRoomRoleAgentPlanFromDatabase,
|
||||
getStoredRoomSettingsFromDatabase,
|
||||
setExplicitRoomModeInDatabase,
|
||||
setRegisteredGroupForTestsInDatabase,
|
||||
@@ -354,6 +355,14 @@ export function getStoredRoomSettings(
|
||||
return getStoredRoomSettingsFromDatabase(db, chatJid);
|
||||
}
|
||||
|
||||
export function getStoredRoomRoleAgentPlan(
|
||||
chatJid: string,
|
||||
): ReturnType<typeof getStoredRoomRoleAgentPlanFromDatabase> {
|
||||
const db = getDatabaseIfInitialized();
|
||||
if (!db) return undefined;
|
||||
return getStoredRoomRoleAgentPlanFromDatabase(db, chatJid);
|
||||
}
|
||||
|
||||
export function getExplicitRoomMode(chatJid: string): RoomMode | undefined {
|
||||
return getExplicitRoomModeFromDatabase(requireDatabase(), chatJid);
|
||||
}
|
||||
|
||||
@@ -565,6 +565,73 @@ describe('IPC message authorization', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('injects authorized inbound messages for the source group without routing them as outbound sends', async () => {
|
||||
const sendMessage = vi.fn(async () => {});
|
||||
const injectInboundMessage = vi.fn(async () => {});
|
||||
|
||||
const result = await (forwardAuthorizedIpcMessage as any)(
|
||||
{
|
||||
type: 'inject_inbound_message',
|
||||
chatJid: 'other@g.us',
|
||||
text: 'act like a human input',
|
||||
senderRole: 'owner',
|
||||
sender: '1480575043007418420',
|
||||
senderName: 'algo',
|
||||
treatAsHuman: true,
|
||||
sourceKind: 'trusted_external_bot',
|
||||
},
|
||||
'other-group',
|
||||
false,
|
||||
groups,
|
||||
sendMessage,
|
||||
injectInboundMessage,
|
||||
);
|
||||
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
expect(injectInboundMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
chatJid: 'other@g.us',
|
||||
text: 'act like a human input',
|
||||
sender: '1480575043007418420',
|
||||
senderName: 'algo',
|
||||
treatAsHuman: true,
|
||||
sourceKind: 'trusted_external_bot',
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
outcome: 'sent',
|
||||
chatJid: 'other@g.us',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not inject unauthorized inbound messages for another group', async () => {
|
||||
const sendMessage = vi.fn(async () => {});
|
||||
const injectInboundMessage = vi.fn(async () => {});
|
||||
|
||||
const result = await (forwardAuthorizedIpcMessage as any)(
|
||||
{
|
||||
type: 'inject_inbound_message',
|
||||
chatJid: 'third@g.us',
|
||||
text: 'should be blocked',
|
||||
sender: '1480575043007418420',
|
||||
senderName: 'algo',
|
||||
treatAsHuman: true,
|
||||
sourceKind: 'trusted_external_bot',
|
||||
},
|
||||
'other-group',
|
||||
false,
|
||||
groups,
|
||||
sendMessage,
|
||||
injectInboundMessage,
|
||||
);
|
||||
|
||||
expect(result.outcome).toBe('blocked');
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
expect(injectInboundMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not forward unauthorized IPC messages even when senderRole exists', async () => {
|
||||
const sendMessage = vi.fn(async () => {});
|
||||
|
||||
@@ -975,6 +1042,39 @@ describe('assign_room success', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('main group can assign per-room reviewer and arbiter agent types', async () => {
|
||||
await processTaskIpc(
|
||||
{
|
||||
type: 'assign_room',
|
||||
jid: 'tribunal-role-overrides@g.us',
|
||||
name: 'Tribunal Role Overrides',
|
||||
room_mode: 'tribunal',
|
||||
owner_agent_type: 'claude-code',
|
||||
reviewer_agent_type: 'codex',
|
||||
arbiter_agent_type: 'claude-code',
|
||||
},
|
||||
'whatsapp_main',
|
||||
true,
|
||||
deps,
|
||||
);
|
||||
|
||||
expect(getStoredRoomSettings('tribunal-role-overrides@g.us')).toMatchObject({
|
||||
chatJid: 'tribunal-role-overrides@g.us',
|
||||
roomMode: 'tribunal',
|
||||
modeSource: 'explicit',
|
||||
name: 'Tribunal Role Overrides',
|
||||
ownerAgentType: 'claude-code',
|
||||
});
|
||||
expect(
|
||||
getRegisteredAgentTypesForJid('tribunal-role-overrides@g.us').sort(),
|
||||
).toEqual(['claude-code', 'codex']);
|
||||
expect(
|
||||
getRegisteredGroup('tribunal-role-overrides@g.us', 'codex'),
|
||||
).toMatchObject({
|
||||
agentType: 'codex',
|
||||
});
|
||||
});
|
||||
|
||||
it('assign_room rejects request with missing fields', async () => {
|
||||
await processTaskIpc(
|
||||
{
|
||||
|
||||
@@ -241,6 +241,69 @@ export async function processTaskIpc(
|
||||
break;
|
||||
}
|
||||
|
||||
if (data.action === 'ejclaw_room_runtime') {
|
||||
if (!data.chatJid) {
|
||||
writeHostEvidenceResponse(sourceGroup, {
|
||||
requestId: data.requestId,
|
||||
ok: false,
|
||||
action: 'ejclaw_room_runtime',
|
||||
command: 'internal:ejclaw_room_runtime',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 1,
|
||||
error: 'Missing chatJid for ejclaw_room_runtime request',
|
||||
});
|
||||
logger.warn(
|
||||
{ sourceGroup, requestId: data.requestId },
|
||||
'Rejected ejclaw_room_runtime request without chatJid',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!deps.getRoomRuntimeReport) {
|
||||
writeHostEvidenceResponse(sourceGroup, {
|
||||
requestId: data.requestId,
|
||||
ok: false,
|
||||
action: 'ejclaw_room_runtime',
|
||||
command: 'internal:ejclaw_room_runtime',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 1,
|
||||
error: 'Room runtime reporting is not configured',
|
||||
});
|
||||
logger.warn(
|
||||
{ sourceGroup, requestId: data.requestId, chatJid: data.chatJid },
|
||||
'Rejected ejclaw_room_runtime request because runtime reporter is unavailable',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const report = deps.getRoomRuntimeReport({
|
||||
chatJid: data.chatJid,
|
||||
sourceGroup,
|
||||
isMain,
|
||||
});
|
||||
writeHostEvidenceResponse(sourceGroup, {
|
||||
requestId: data.requestId,
|
||||
ok: true,
|
||||
action: 'ejclaw_room_runtime',
|
||||
command: 'internal:ejclaw_room_runtime',
|
||||
stdout: JSON.stringify(report, null, 2),
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
});
|
||||
logger.info(
|
||||
{
|
||||
sourceGroup,
|
||||
requestId: data.requestId,
|
||||
action: data.action,
|
||||
chatJid: data.chatJid,
|
||||
},
|
||||
'Processed room runtime report request via IPC',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isHostEvidenceAction(data.action)) {
|
||||
writeHostEvidenceResponse(sourceGroup, {
|
||||
requestId: data.requestId,
|
||||
@@ -405,11 +468,36 @@ export async function processTaskIpc(
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (
|
||||
data.reviewer_agent_type !== undefined &&
|
||||
data.reviewer_agent_type !== 'claude-code' &&
|
||||
data.reviewer_agent_type !== 'codex'
|
||||
) {
|
||||
logger.warn(
|
||||
{ sourceGroup, reviewerAgentType: data.reviewer_agent_type },
|
||||
'Invalid assign_room request - unknown reviewer_agent_type',
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (
|
||||
data.arbiter_agent_type !== undefined &&
|
||||
data.arbiter_agent_type !== null &&
|
||||
data.arbiter_agent_type !== 'claude-code' &&
|
||||
data.arbiter_agent_type !== 'codex'
|
||||
) {
|
||||
logger.warn(
|
||||
{ sourceGroup, arbiterAgentType: data.arbiter_agent_type },
|
||||
'Invalid assign_room request - unknown arbiter_agent_type',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
deps.assignRoom(data.jid, {
|
||||
name: data.name,
|
||||
roomMode: data.room_mode,
|
||||
ownerAgentType: data.owner_agent_type,
|
||||
reviewerAgentType: data.reviewer_agent_type,
|
||||
arbiterAgentType: data.arbiter_agent_type,
|
||||
folder: data.folder,
|
||||
isMain: data.isMain,
|
||||
workDir: data.workDir,
|
||||
|
||||
@@ -1,6 +1,65 @@
|
||||
import type { AvailableGroup } from './agent-runner.js';
|
||||
import type { AssignRoomInput } from './db.js';
|
||||
import type { AgentType, RegisteredGroup, RoomMode } from './types.js';
|
||||
import type {
|
||||
AgentType,
|
||||
MessageSourceKind,
|
||||
PairedTask,
|
||||
RegisteredGroup,
|
||||
RoomMode,
|
||||
} from './types.js';
|
||||
|
||||
export interface InjectInboundMessagePayload {
|
||||
chatJid: string;
|
||||
text: string;
|
||||
sender?: string;
|
||||
senderName?: string;
|
||||
messageId?: string;
|
||||
timestamp?: string;
|
||||
treatAsHuman: boolean;
|
||||
sourceKind?: MessageSourceKind;
|
||||
}
|
||||
|
||||
export interface RoomRuntimeReport {
|
||||
chatJid: string;
|
||||
groupFolder: string;
|
||||
room: RegisteredGroup | null;
|
||||
queue: {
|
||||
status: 'processing' | 'waiting' | 'inactive';
|
||||
runPhase: string;
|
||||
elapsedMs: number | null;
|
||||
pendingMessages: boolean;
|
||||
pendingTasks: number;
|
||||
currentRunId: string | null;
|
||||
runningTaskId: string | null;
|
||||
processName: string | null;
|
||||
ipcDir: string | null;
|
||||
retryCount: number;
|
||||
retryScheduledAt: number | null;
|
||||
waiting: boolean;
|
||||
};
|
||||
latestOpenTask: Pick<
|
||||
PairedTask,
|
||||
| 'id'
|
||||
| 'status'
|
||||
| 'title'
|
||||
| 'round_trip_count'
|
||||
| 'updated_at'
|
||||
| 'owner_agent_type'
|
||||
| 'reviewer_agent_type'
|
||||
| 'arbiter_agent_type'
|
||||
> | null;
|
||||
recentMessages: Array<{
|
||||
id: string;
|
||||
seq?: number;
|
||||
timestamp: string;
|
||||
sender: string;
|
||||
senderName: string;
|
||||
isFromMe: boolean;
|
||||
isBotMessage: boolean;
|
||||
sourceKind: MessageSourceKind;
|
||||
contentPreview: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IpcDeps {
|
||||
sendMessage: (
|
||||
@@ -19,6 +78,12 @@ export interface IpcDeps {
|
||||
isMain: boolean,
|
||||
availableGroups: AvailableGroup[],
|
||||
) => void;
|
||||
injectInboundMessage?: (payload: InjectInboundMessagePayload) => Promise<void>;
|
||||
getRoomRuntimeReport?: (args: {
|
||||
chatJid: string;
|
||||
sourceGroup: string;
|
||||
isMain: boolean;
|
||||
}) => RoomRuntimeReport;
|
||||
}
|
||||
|
||||
export interface IpcMessagePayload {
|
||||
@@ -27,6 +92,12 @@ export interface IpcMessagePayload {
|
||||
text?: string;
|
||||
senderRole?: string;
|
||||
runId?: string;
|
||||
sender?: string;
|
||||
senderName?: string;
|
||||
messageId?: string;
|
||||
timestamp?: string;
|
||||
treatAsHuman?: boolean;
|
||||
sourceKind?: MessageSourceKind;
|
||||
}
|
||||
|
||||
export interface IpcMessageForwardResult {
|
||||
@@ -54,6 +125,8 @@ export interface TaskIpcPayload {
|
||||
folder?: string;
|
||||
room_mode?: RoomMode;
|
||||
owner_agent_type?: AgentType;
|
||||
reviewer_agent_type?: AgentType;
|
||||
arbiter_agent_type?: AgentType | null;
|
||||
isMain?: boolean;
|
||||
workDir?: string;
|
||||
scopeKind?: string;
|
||||
|
||||
@@ -315,6 +315,28 @@ describe('message-runtime-rules', () => {
|
||||
expect(resolution.effectiveServiceId).toBe(resolution.reviewerServiceId);
|
||||
});
|
||||
|
||||
it('uses lease reviewer metadata when the room reviewer differs from the owner agent type', () => {
|
||||
const resolution = resolveExecutionTarget({
|
||||
lease: {
|
||||
...baseLease,
|
||||
owner_agent_type: 'claude-code',
|
||||
reviewer_agent_type: 'codex',
|
||||
reviewer_service_id: 'codex-review',
|
||||
},
|
||||
pairedTaskStatus: 'review_ready',
|
||||
groupFolder: 'group-1',
|
||||
groupAgentType: 'claude-code',
|
||||
});
|
||||
|
||||
expect(resolution).toMatchObject({
|
||||
activeRole: 'reviewer',
|
||||
configuredAgentType: 'codex',
|
||||
effectiveAgentType: 'codex',
|
||||
sessionFolder: 'group-1:reviewer',
|
||||
});
|
||||
expect(resolution.effectiveServiceId).toBe('codex-review');
|
||||
});
|
||||
|
||||
it('resolves merge_ready execution target back to the owner/finalize path', () => {
|
||||
const resolution = resolveExecutionTarget({
|
||||
lease: baseLease,
|
||||
@@ -346,8 +368,8 @@ describe('message-runtime-rules', () => {
|
||||
expect(resolution).toMatchObject({
|
||||
inferredRole: 'arbiter',
|
||||
activeRole: 'arbiter',
|
||||
configuredAgentType: 'claude-code',
|
||||
effectiveAgentType: 'claude-code',
|
||||
configuredAgentType: 'codex',
|
||||
effectiveAgentType: 'codex',
|
||||
sessionFolder: 'group-1:arbiter',
|
||||
});
|
||||
expect(resolution.effectiveServiceId).toBe(
|
||||
@@ -355,6 +377,30 @@ describe('message-runtime-rules', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('uses lease arbiter metadata when the room arbiter differs from the owner agent type', () => {
|
||||
const resolution = resolveExecutionTarget({
|
||||
lease: {
|
||||
...baseLease,
|
||||
owner_agent_type: 'codex',
|
||||
reviewer_agent_type: 'claude-code',
|
||||
reviewer_service_id: 'claude',
|
||||
arbiter_agent_type: 'claude-code',
|
||||
arbiter_service_id: 'claude',
|
||||
},
|
||||
pairedTaskStatus: 'arbiter_requested',
|
||||
groupFolder: 'group-1',
|
||||
groupAgentType: 'codex',
|
||||
});
|
||||
|
||||
expect(resolution).toMatchObject({
|
||||
activeRole: 'arbiter',
|
||||
configuredAgentType: 'claude-code',
|
||||
effectiveAgentType: 'claude-code',
|
||||
sessionFolder: 'group-1:arbiter',
|
||||
});
|
||||
expect(resolution.effectiveServiceId).toBe('claude');
|
||||
});
|
||||
|
||||
it('honors an arbiter forced role only when arbiter is configured', () => {
|
||||
const resolution = resolveExecutionTarget({
|
||||
lease: {
|
||||
|
||||
@@ -248,12 +248,14 @@ export function resolveCursorKeyForRole(
|
||||
export function resolveConfiguredRoleAgentPlan(
|
||||
paired: boolean,
|
||||
groupAgentType: AgentType | undefined,
|
||||
configuredReviewer: AgentType = REVIEWER_AGENT_TYPE,
|
||||
configuredArbiter: AgentType | null | undefined = ARBITER_AGENT_TYPE,
|
||||
): RoleAgentPlan {
|
||||
return resolveRoleAgentPlan({
|
||||
paired,
|
||||
groupAgentType,
|
||||
configuredReviewer: REVIEWER_AGENT_TYPE,
|
||||
configuredArbiter: ARBITER_AGENT_TYPE,
|
||||
configuredReviewer,
|
||||
configuredArbiter,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -261,8 +263,15 @@ export function resolveConfiguredRoleAgentPlan(
|
||||
export function resolveEffectiveAgentType(
|
||||
role: 'owner' | 'reviewer' | 'arbiter',
|
||||
groupAgentType: AgentType | undefined,
|
||||
reviewerAgentType: AgentType = REVIEWER_AGENT_TYPE,
|
||||
arbiterAgentType: AgentType | null | undefined = ARBITER_AGENT_TYPE,
|
||||
): AgentType {
|
||||
const plan = resolveConfiguredRoleAgentPlan(role !== 'owner', groupAgentType);
|
||||
const plan = resolveConfiguredRoleAgentPlan(
|
||||
role !== 'owner',
|
||||
groupAgentType,
|
||||
reviewerAgentType,
|
||||
arbiterAgentType,
|
||||
);
|
||||
return resolveAgentTypeForRole(plan, role);
|
||||
}
|
||||
|
||||
@@ -271,10 +280,17 @@ export function resolveSessionFolder(
|
||||
groupFolder: string,
|
||||
role: 'owner' | 'reviewer' | 'arbiter',
|
||||
groupAgentType: AgentType | undefined,
|
||||
reviewerAgentType: AgentType = REVIEWER_AGENT_TYPE,
|
||||
arbiterAgentType: AgentType | null | undefined = ARBITER_AGENT_TYPE,
|
||||
): string {
|
||||
// Arbiter always gets a separate session — must never share with owner/reviewer
|
||||
if (role === 'arbiter') return `${groupFolder}:arbiter`;
|
||||
const plan = resolveConfiguredRoleAgentPlan(role !== 'owner', groupAgentType);
|
||||
const plan = resolveConfiguredRoleAgentPlan(
|
||||
role !== 'owner',
|
||||
groupAgentType,
|
||||
reviewerAgentType,
|
||||
arbiterAgentType,
|
||||
);
|
||||
const effectiveType = resolveAgentTypeForRole(plan, role);
|
||||
const groupDefault = plan.ownerAgentType;
|
||||
if (effectiveType === groupDefault) return groupFolder;
|
||||
@@ -316,14 +332,14 @@ export function resolveExecutionTarget(args: {
|
||||
|
||||
const reviewerServiceId = resolveLeaseServiceId(args.lease, 'reviewer');
|
||||
const arbiterServiceId = resolveLeaseServiceId(args.lease, 'arbiter');
|
||||
const ownerAgentType = args.lease.owner_agent_type ?? args.groupAgentType;
|
||||
const roleAgentPlan = resolveConfiguredRoleAgentPlan(
|
||||
args.lease.reviewer_agent_type != null,
|
||||
args.groupAgentType,
|
||||
);
|
||||
const configuredAgentType = resolveEffectiveAgentType(
|
||||
activeRole,
|
||||
args.groupAgentType,
|
||||
reviewerServiceId != null,
|
||||
ownerAgentType,
|
||||
args.lease.reviewer_agent_type ?? REVIEWER_AGENT_TYPE,
|
||||
args.lease.arbiter_agent_type ?? ARBITER_AGENT_TYPE,
|
||||
);
|
||||
const configuredAgentType = resolveAgentTypeForRole(roleAgentPlan, activeRole);
|
||||
const effectiveAgentType = args.forcedAgentType ?? configuredAgentType;
|
||||
|
||||
return {
|
||||
@@ -339,7 +355,9 @@ export function resolveExecutionTarget(args: {
|
||||
sessionFolder: resolveSessionFolder(
|
||||
args.groupFolder,
|
||||
activeRole,
|
||||
args.groupAgentType,
|
||||
roleAgentPlan.ownerAgentType,
|
||||
roleAgentPlan.reviewerAgentType ?? REVIEWER_AGENT_TYPE,
|
||||
roleAgentPlan.arbiterAgentType ?? ARBITER_AGENT_TYPE,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -250,6 +250,32 @@ describe('paired execution context', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses room role context agent overrides when creating a paired task', () => {
|
||||
preparePairedExecutionContext({
|
||||
group,
|
||||
chatJid: 'dc:test',
|
||||
runId: 'run-room-overrides',
|
||||
roomRoleContext: {
|
||||
...ownerContext,
|
||||
ownerAgentType: 'codex',
|
||||
reviewerAgentType: 'codex',
|
||||
reviewerServiceId: config.CODEX_REVIEW_SERVICE_ID,
|
||||
arbiterAgentType: 'claude-code',
|
||||
},
|
||||
hasHumanMessage: true,
|
||||
});
|
||||
|
||||
expect(db.createPairedTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
owner_service_id: config.CODEX_MAIN_SERVICE_ID,
|
||||
reviewer_service_id: config.CODEX_REVIEW_SERVICE_ID,
|
||||
owner_agent_type: 'codex',
|
||||
reviewer_agent_type: 'codex',
|
||||
arbiter_agent_type: 'claude-code',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('persists stable role-slot service shadow instead of the transient failover owner lease', () => {
|
||||
preparePairedExecutionContext({
|
||||
group,
|
||||
@@ -562,6 +588,49 @@ describe('paired execution context', () => {
|
||||
delete process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE;
|
||||
});
|
||||
|
||||
it('honors room-level reviewer agent overrides for unsafe host reviewer runtime env', () => {
|
||||
process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE = '1';
|
||||
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(
|
||||
buildPairedTask({
|
||||
status: 'review_ready',
|
||||
review_requested_at: '2026-03-28T00:00:00.000Z',
|
||||
reviewer_agent_type: 'codex',
|
||||
}),
|
||||
);
|
||||
vi.mocked(db.getPairedTaskById).mockReturnValue(
|
||||
buildPairedTask({
|
||||
status: 'review_ready',
|
||||
review_requested_at: '2026-03-28T00:00:00.000Z',
|
||||
reviewer_agent_type: 'codex',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
pairedWorkspaceManager.prepareReviewerWorkspaceForExecution,
|
||||
).mockReturnValue({
|
||||
workspace: buildWorkspace('reviewer', '/tmp/paired/task-1/reviewer'),
|
||||
autoRefreshed: false,
|
||||
});
|
||||
|
||||
const result = preparePairedExecutionContext({
|
||||
group,
|
||||
chatJid: 'dc:test',
|
||||
runId: 'run-host-reviewer-room-override',
|
||||
roomRoleContext: {
|
||||
...reviewerContext,
|
||||
reviewerAgentType: 'codex',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result?.envOverrides).toMatchObject({
|
||||
EJCLAW_WORK_DIR: '/tmp/paired/task-1/reviewer',
|
||||
EJCLAW_PAIRED_ROLE: 'reviewer',
|
||||
EJCLAW_UNSAFE_HOST_PAIRED_MODE: '1',
|
||||
});
|
||||
expect(result?.envOverrides.EJCLAW_CLAUDE_REVIEWER_READONLY).toBeUndefined();
|
||||
expect(result?.envOverrides.EJCLAW_REVIEWER_RUNTIME).toBeUndefined();
|
||||
delete process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE;
|
||||
});
|
||||
|
||||
it('completePairedExecutionContext logs without error', () => {
|
||||
completePairedExecutionContext({
|
||||
taskId: 'task-1',
|
||||
|
||||
@@ -106,14 +106,17 @@ function createActiveTaskForRoom(args: {
|
||||
group: RegisteredGroup;
|
||||
chatJid: string;
|
||||
canonicalWorkDir: string;
|
||||
roomRoleContext?: RoomRoleContext;
|
||||
}): PairedTask {
|
||||
const { group, chatJid, canonicalWorkDir } = args;
|
||||
const { group, chatJid, canonicalWorkDir, roomRoleContext } = args;
|
||||
const now = new Date().toISOString();
|
||||
const rolePlan = resolveRoleAgentPlan({
|
||||
paired: true,
|
||||
groupAgentType: group.agentType,
|
||||
configuredReviewer: REVIEWER_AGENT_TYPE,
|
||||
configuredArbiter: ARBITER_AGENT_TYPE,
|
||||
groupAgentType: roomRoleContext?.ownerAgentType ?? group.agentType,
|
||||
configuredReviewer:
|
||||
roomRoleContext?.reviewerAgentType ?? REVIEWER_AGENT_TYPE,
|
||||
configuredArbiter:
|
||||
roomRoleContext?.arbiterAgentType ?? ARBITER_AGENT_TYPE,
|
||||
});
|
||||
const ownerServiceShadow = resolvePairedTaskServiceShadow(
|
||||
'owner',
|
||||
@@ -195,6 +198,7 @@ export interface ResolvedOwnerHumanTask {
|
||||
export function resolveOwnerTaskForHumanMessage(args: {
|
||||
group: RegisteredGroup;
|
||||
chatJid: string;
|
||||
roomRoleContext?: RoomRoleContext;
|
||||
existingTask?: PairedTask | null;
|
||||
}): ResolvedOwnerHumanTask {
|
||||
const canonicalWorkDir = ensurePairedProject(args.group, args.chatJid);
|
||||
@@ -208,6 +212,7 @@ export function resolveOwnerTaskForHumanMessage(args: {
|
||||
group: args.group,
|
||||
chatJid: args.chatJid,
|
||||
canonicalWorkDir,
|
||||
roomRoleContext: args.roomRoleContext,
|
||||
})
|
||||
: null,
|
||||
supersededTask: null,
|
||||
@@ -247,6 +252,7 @@ export function resolveOwnerTaskForHumanMessage(args: {
|
||||
group: args.group,
|
||||
chatJid: args.chatJid,
|
||||
canonicalWorkDir,
|
||||
roomRoleContext: args.roomRoleContext,
|
||||
}),
|
||||
supersededTask: existing,
|
||||
};
|
||||
@@ -267,6 +273,7 @@ function ensureActiveTask(
|
||||
return resolveOwnerTaskForHumanMessage({
|
||||
group,
|
||||
chatJid,
|
||||
roomRoleContext,
|
||||
existingTask: existing ?? null,
|
||||
}).task;
|
||||
}
|
||||
@@ -287,6 +294,7 @@ function ensureActiveTask(
|
||||
group,
|
||||
chatJid,
|
||||
canonicalWorkDir,
|
||||
roomRoleContext,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -470,7 +478,7 @@ export function preparePairedExecutionContext(args: {
|
||||
envOverrides,
|
||||
buildPairedReadonlyRuntimeEnvOverrides({
|
||||
role: 'reviewer',
|
||||
agentType: REVIEWER_AGENT_TYPE,
|
||||
agentType: roomRoleContext.reviewerAgentType ?? REVIEWER_AGENT_TYPE,
|
||||
unsafeHostPairedMode,
|
||||
}),
|
||||
);
|
||||
@@ -489,7 +497,10 @@ export function preparePairedExecutionContext(args: {
|
||||
envOverrides,
|
||||
buildPairedReadonlyRuntimeEnvOverrides({
|
||||
role: 'arbiter',
|
||||
agentType: ARBITER_AGENT_TYPE ?? REVIEWER_AGENT_TYPE,
|
||||
agentType:
|
||||
roomRoleContext.arbiterAgentType ??
|
||||
ARBITER_AGENT_TYPE ??
|
||||
REVIEWER_AGENT_TYPE,
|
||||
unsafeHostPairedMode,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
_setRegisteredGroupForTests,
|
||||
_setStoredRoomOwnerAgentTypeForTests,
|
||||
_initTestDatabase,
|
||||
assignRoom,
|
||||
setChannelOwnerLease,
|
||||
setExplicitRoomMode,
|
||||
} from './db.js';
|
||||
@@ -236,6 +237,29 @@ describe('service-routing global failover', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses room-level reviewer and arbiter agent overrides from assign_room', () => {
|
||||
assignRoom('dc:room-role-overrides', {
|
||||
name: 'Room Role Overrides',
|
||||
roomMode: 'tribunal',
|
||||
ownerAgentType: 'claude-code',
|
||||
reviewerAgentType: 'codex',
|
||||
arbiterAgentType: 'claude-code',
|
||||
folder: 'room-role-overrides',
|
||||
});
|
||||
|
||||
expect(getEffectiveChannelLease('dc:room-role-overrides')).toMatchObject({
|
||||
chat_jid: 'dc:room-role-overrides',
|
||||
owner_agent_type: 'claude-code',
|
||||
reviewer_agent_type: 'codex',
|
||||
arbiter_agent_type: 'claude-code',
|
||||
owner_service_id: 'claude',
|
||||
reviewer_service_id: 'codex-review',
|
||||
arbiter_service_id: 'claude',
|
||||
owner_failover_active: false,
|
||||
explicit: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to the configured owner service for chats without canonical room settings', () => {
|
||||
expect(getEffectiveChannelLease('dc:unregistered')).toMatchObject({
|
||||
chat_jid: 'dc:unregistered',
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
clearChannelOwnerLease,
|
||||
getAllChannelOwnerLeases,
|
||||
getEffectiveRuntimeRoomMode,
|
||||
getStoredRoomRoleAgentPlan,
|
||||
getStoredRoomSettings,
|
||||
type ChannelOwnerLeaseRow,
|
||||
} from './db.js';
|
||||
@@ -46,8 +47,10 @@ function normalizeLeaseRow(
|
||||
row: ChannelOwnerLeaseRow,
|
||||
explicit: boolean,
|
||||
): EffectiveChannelLease {
|
||||
const storedRolePlan = getStoredRoomRoleAgentPlan(row.chat_jid);
|
||||
const ownerAgentType =
|
||||
row.owner_agent_type ??
|
||||
storedRolePlan?.ownerAgentType ??
|
||||
getStoredRoomSettings(row.chat_jid)?.ownerAgentType ??
|
||||
inferAgentTypeFromServiceShadow(row.owner_service_id) ??
|
||||
'claude-code';
|
||||
@@ -56,6 +59,7 @@ function normalizeLeaseRow(
|
||||
? null
|
||||
: (row.reviewer_agent_type ??
|
||||
inferAgentTypeFromServiceShadow(row.reviewer_service_id) ??
|
||||
storedRolePlan?.reviewerAgentType ??
|
||||
resolveRoleAgentPlan({
|
||||
paired: true,
|
||||
groupAgentType: ownerAgentType,
|
||||
@@ -67,6 +71,7 @@ function normalizeLeaseRow(
|
||||
(row.arbiter_service_id
|
||||
? inferAgentTypeFromServiceShadow(row.arbiter_service_id)
|
||||
: undefined) ??
|
||||
storedRolePlan?.arbiterAgentType ??
|
||||
null;
|
||||
|
||||
return {
|
||||
@@ -102,20 +107,21 @@ function normalizeLeaseRow(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDefaultOwnerAgentType(chatJid: string): AgentType | undefined {
|
||||
return getStoredRoomSettings(chatJid)?.ownerAgentType;
|
||||
}
|
||||
|
||||
function getDefaultLease(chatJid: string): EffectiveChannelLease {
|
||||
const roomMode = getEffectiveRuntimeRoomMode(chatJid);
|
||||
const storedRolePlan = getStoredRoomRoleAgentPlan(chatJid);
|
||||
const ownerAgentType =
|
||||
resolveDefaultOwnerAgentType(chatJid) ?? OWNER_AGENT_TYPE;
|
||||
const rolePlan = resolveRoleAgentPlan({
|
||||
paired: roomMode === 'tribunal',
|
||||
groupAgentType: ownerAgentType,
|
||||
configuredReviewer: REVIEWER_AGENT_TYPE,
|
||||
configuredArbiter: ARBITER_AGENT_TYPE,
|
||||
});
|
||||
storedRolePlan?.ownerAgentType ??
|
||||
getStoredRoomSettings(chatJid)?.ownerAgentType ??
|
||||
OWNER_AGENT_TYPE;
|
||||
const rolePlan =
|
||||
storedRolePlan ??
|
||||
resolveRoleAgentPlan({
|
||||
paired: roomMode === 'tribunal',
|
||||
groupAgentType: ownerAgentType,
|
||||
configuredReviewer: REVIEWER_AGENT_TYPE,
|
||||
configuredArbiter: ARBITER_AGENT_TYPE,
|
||||
});
|
||||
|
||||
return {
|
||||
chat_jid: chatJid,
|
||||
|
||||
Reference in New Issue
Block a user