refactor: cut over room mode from paired legacy
This commit is contained in:
@@ -17,10 +17,6 @@ vi.mock('./config.js', () => ({
|
|||||||
isReviewService: vi.fn(() => false),
|
isReviewService: vi.fn(() => false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./db.js', () => ({
|
|
||||||
isPairedRoomJid: vi.fn(() => false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('./env.js', () => ({
|
vi.mock('./env.js', () => ({
|
||||||
readEnvFile: mockReadEnvFile,
|
readEnvFile: mockReadEnvFile,
|
||||||
getEnv: vi.fn((key: string) => undefined),
|
getEnv: vi.fn((key: string) => undefined),
|
||||||
@@ -40,6 +36,7 @@ vi.mock('./platform-prompts.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./service-routing.js', () => ({
|
vi.mock('./service-routing.js', () => ({
|
||||||
|
hasReviewerLease: vi.fn(() => false),
|
||||||
getEffectiveChannelLease: vi.fn(() => ({
|
getEffectiveChannelLease: vi.fn(() => ({
|
||||||
chat_jid: 'dc:test',
|
chat_jid: 'dc:test',
|
||||||
owner_service_id: 'claude',
|
owner_service_id: 'claude',
|
||||||
@@ -82,7 +79,6 @@ vi.mock('os', async () => {
|
|||||||
|
|
||||||
import { prepareGroupEnvironment } from './agent-runner-environment.js';
|
import { prepareGroupEnvironment } from './agent-runner-environment.js';
|
||||||
import * as config from './config.js';
|
import * as config from './config.js';
|
||||||
import * as db from './db.js';
|
|
||||||
import * as serviceRouting from './service-routing.js';
|
import * as serviceRouting from './service-routing.js';
|
||||||
import type { RegisteredGroup } from './types.js';
|
import type { RegisteredGroup } from './types.js';
|
||||||
|
|
||||||
@@ -202,7 +198,7 @@ describe('prepareGroupEnvironment codex auth handling', () => {
|
|||||||
|
|
||||||
it('uses the failover owner prompt pack for codex-review when it owns an explicit failover lease', () => {
|
it('uses the failover owner prompt pack for codex-review when it owns an explicit failover lease', () => {
|
||||||
vi.mocked(config.isReviewService).mockReturnValue(true);
|
vi.mocked(config.isReviewService).mockReturnValue(true);
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
||||||
chat_jid: 'dc:test',
|
chat_jid: 'dc:test',
|
||||||
owner_service_id: 'codex-review',
|
owner_service_id: 'codex-review',
|
||||||
@@ -262,7 +258,7 @@ describe('prepareGroupEnvironment codex auth handling', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds only the shared owner prompt fragments to Claude session prompts', () => {
|
it('adds only the shared owner prompt fragments to Claude session prompts', () => {
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
mockReadEnvFile.mockReturnValue({});
|
mockReadEnvFile.mockReturnValue({});
|
||||||
|
|
||||||
const promptsDir = path.join(tempRoot, 'prompts');
|
const promptsDir = path.join(tempRoot, 'prompts');
|
||||||
@@ -307,7 +303,7 @@ describe('prepareGroupEnvironment codex auth handling', () => {
|
|||||||
|
|
||||||
it('returns to the normal owner prompt stack after failover is cleared', () => {
|
it('returns to the normal owner prompt stack after failover is cleared', () => {
|
||||||
vi.mocked(config.isReviewService).mockReturnValue(true);
|
vi.mocked(config.isReviewService).mockReturnValue(true);
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
||||||
chat_jid: 'dc:test',
|
chat_jid: 'dc:test',
|
||||||
owner_service_id: 'claude',
|
owner_service_id: 'claude',
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
isReviewService,
|
isReviewService,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { isPairedRoomJid } from './db.js';
|
|
||||||
import { readEnvFile } from './env.js';
|
import { readEnvFile } from './env.js';
|
||||||
import { getActiveCodexAuthPath } from './codex-token-rotation.js';
|
import { getActiveCodexAuthPath } from './codex-token-rotation.js';
|
||||||
import { getCurrentToken } from './token-rotation.js';
|
import { getCurrentToken } from './token-rotation.js';
|
||||||
@@ -27,7 +26,7 @@ import {
|
|||||||
readPairedRoomPrompt,
|
readPairedRoomPrompt,
|
||||||
readPlatformPrompt,
|
readPlatformPrompt,
|
||||||
} from './platform-prompts.js';
|
} from './platform-prompts.js';
|
||||||
import { getEffectiveChannelLease } from './service-routing.js';
|
import { getEffectiveChannelLease, hasReviewerLease } from './service-routing.js';
|
||||||
import type { AgentType, RegisteredGroup } from './types.js';
|
import type { AgentType, RegisteredGroup } from './types.js';
|
||||||
|
|
||||||
// writeCodexApiKeyAuth removed — Codex uses OAuth only.
|
// writeCodexApiKeyAuth removed — Codex uses OAuth only.
|
||||||
@@ -418,7 +417,7 @@ export function prepareGroupEnvironment(
|
|||||||
|
|
||||||
const globalDir = path.join(GROUPS_DIR, 'global');
|
const globalDir = path.join(GROUPS_DIR, 'global');
|
||||||
const globalClaudeMdPath = path.join(globalDir, 'CLAUDE.md');
|
const globalClaudeMdPath = path.join(globalDir, 'CLAUDE.md');
|
||||||
const isPairedRoom = isPairedRoomJid(chatJid);
|
const isPairedRoom = hasReviewerLease(chatJid);
|
||||||
const effectiveLease = getEffectiveChannelLease(chatJid);
|
const effectiveLease = getEffectiveChannelLease(chatJid);
|
||||||
const useCodexReviewFailoverPromptPack =
|
const useCodexReviewFailoverPromptPack =
|
||||||
isReviewService(SERVICE_ID) &&
|
isReviewService(SERVICE_ID) &&
|
||||||
@@ -551,7 +550,7 @@ export function prepareContainerSessionEnvironment(args: {
|
|||||||
|
|
||||||
// Build CLAUDE.md with role-appropriate prompts (reviewer or arbiter)
|
// Build CLAUDE.md with role-appropriate prompts (reviewer or arbiter)
|
||||||
const claudePlatformPrompt = readPlatformPrompt('claude-code', projectRoot);
|
const claudePlatformPrompt = readPlatformPrompt('claude-code', projectRoot);
|
||||||
const claudePairedRoomPrompt = isPairedRoomJid(chatJid)
|
const claudePairedRoomPrompt = hasReviewerLease(chatJid)
|
||||||
? role === 'arbiter'
|
? role === 'arbiter'
|
||||||
? readArbiterPrompt(projectRoot)
|
? readArbiterPrompt(projectRoot)
|
||||||
: readPairedRoomPrompt('claude-code', projectRoot)
|
: readPairedRoomPrompt('claude-code', projectRoot)
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ vi.mock('./service-routing.js', () => ({
|
|||||||
reason: null,
|
reason: null,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
})),
|
})),
|
||||||
|
hasReviewerLease: vi.fn(() => true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create a controllable fake ChildProcess
|
// Create a controllable fake ChildProcess
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ vi.mock('../logger.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const isPairedRoomJidMock = vi.hoisted(() => vi.fn(() => false));
|
const hasReviewerLeaseMock = vi.hoisted(() => vi.fn(() => false));
|
||||||
|
|
||||||
vi.mock('../db.js', () => ({
|
vi.mock('../service-routing.js', () => ({
|
||||||
isPairedRoomJid: isPairedRoomJidMock,
|
hasReviewerLease: hasReviewerLeaseMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// --- discord.js mock ---
|
// --- discord.js mock ---
|
||||||
@@ -206,7 +206,7 @@ async function triggerMessage(message: any) {
|
|||||||
describe('DiscordChannel', () => {
|
describe('DiscordChannel', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
isPairedRoomJidMock.mockReturnValue(false);
|
hasReviewerLeaseMock.mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -343,7 +343,7 @@ describe('DiscordChannel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('delivers other bot messages in paired rooms', async () => {
|
it('delivers other bot messages in paired rooms', async () => {
|
||||||
isPairedRoomJidMock.mockReturnValue(true);
|
hasReviewerLeaseMock.mockReturnValue(true);
|
||||||
|
|
||||||
const opts = createTestOpts();
|
const opts = createTestOpts();
|
||||||
const channel = new DiscordChannel('test-token', opts);
|
const channel = new DiscordChannel('test-token', opts);
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import {
|
|||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
TRIGGER_PATTERN,
|
TRIGGER_PATTERN,
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
import { isPairedRoomJid } from '../db.js';
|
|
||||||
import { getEnv } from '../env.js';
|
import { getEnv } from '../env.js';
|
||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
import { formatOutbound } from '../router.js';
|
import { formatOutbound } from '../router.js';
|
||||||
|
import { hasReviewerLease } from '../service-routing.js';
|
||||||
|
|
||||||
const ATTACHMENTS_DIR = path.join(DATA_DIR, 'attachments');
|
const ATTACHMENTS_DIR = path.join(DATA_DIR, 'attachments');
|
||||||
const TRANSCRIPTION_CACHE_DIR = path.join(CACHE_DIR, 'transcriptions');
|
const TRANSCRIPTION_CACHE_DIR = path.join(CACHE_DIR, 'transcriptions');
|
||||||
@@ -214,7 +214,7 @@ export class DiscordChannel implements Channel {
|
|||||||
const chatJid = `dc:${channelId}`;
|
const chatJid = `dc:${channelId}`;
|
||||||
const isOwnBotMessage = message.author.id === this.client?.user?.id;
|
const isOwnBotMessage = message.author.id === this.client?.user?.id;
|
||||||
if (isOwnBotMessage) return;
|
if (isOwnBotMessage) return;
|
||||||
if (message.author.bot && !isPairedRoomJid(chatJid)) return;
|
if (message.author.bot && !hasReviewerLease(chatJid)) return;
|
||||||
|
|
||||||
let content = message.content;
|
let content = message.content;
|
||||||
const timestamp = message.createdAt.toISOString();
|
const timestamp = message.createdAt.toISOString();
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { Database } from 'bun:sqlite';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
_initTestDatabase,
|
_initTestDatabase,
|
||||||
|
_initTestDatabaseFromFile,
|
||||||
claimServiceHandoff,
|
claimServiceHandoff,
|
||||||
completeServiceHandoffAndAdvanceTargetCursor,
|
completeServiceHandoffAndAdvanceTargetCursor,
|
||||||
createPairedTask,
|
createPairedTask,
|
||||||
@@ -791,6 +794,8 @@ describe('paired room registration', () => {
|
|||||||
'claude-code',
|
'claude-code',
|
||||||
'codex',
|
'codex',
|
||||||
]);
|
]);
|
||||||
|
expect(getExplicitRoomMode('dc:123')).toBeUndefined();
|
||||||
|
expect(getEffectiveRoomMode('dc:123')).toBe('tribunal');
|
||||||
expect(isPairedRoomJid('dc:123')).toBe(true);
|
expect(isPairedRoomJid('dc:123')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -807,7 +812,7 @@ describe('paired room registration', () => {
|
|||||||
expect(isPairedRoomJid('dc:solo')).toBe(false);
|
expect(isPairedRoomJid('dc:solo')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to inferred room mode when no explicit room mode 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 Claude',
|
||||||
folder: 'legacy-paired-claude',
|
folder: 'legacy-paired-claude',
|
||||||
@@ -829,6 +834,74 @@ describe('paired room registration', () => {
|
|||||||
expect(isPairedRoomJid('dc:legacy-paired')).toBe(true);
|
expect(isPairedRoomJid('dc:legacy-paired')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('backfills inferred room modes for legacy SQL rows missing room_settings', () => {
|
||||||
|
const tempDir = fs.mkdtempSync('/tmp/ejclaw-room-mode-');
|
||||||
|
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:legacy-sql',
|
||||||
|
'Legacy SQL Claude',
|
||||||
|
'legacy-sql-claude',
|
||||||
|
'@Andy',
|
||||||
|
'2024-01-01T00:00:00.000Z',
|
||||||
|
'claude-code',
|
||||||
|
);
|
||||||
|
insertGroup.run(
|
||||||
|
'dc:legacy-sql',
|
||||||
|
'Legacy SQL Codex',
|
||||||
|
'legacy-sql-codex',
|
||||||
|
'@Codex',
|
||||||
|
'2024-01-01T00:00:00.000Z',
|
||||||
|
'codex',
|
||||||
|
);
|
||||||
|
legacyDb.close();
|
||||||
|
|
||||||
|
_initTestDatabaseFromFile(dbPath);
|
||||||
|
|
||||||
|
expect(getExplicitRoomMode('dc:legacy-sql')).toBeUndefined();
|
||||||
|
expect(getEffectiveRoomMode('dc:legacy-sql')).toBe('tribunal');
|
||||||
|
expect(getEffectiveRuntimeRoomMode('dc:legacy-sql')).toBe('tribunal');
|
||||||
|
expect(isPairedRoomJid('dc:legacy-sql')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
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 Claude',
|
||||||
|
|||||||
135
src/db.ts
135
src/db.ts
@@ -38,6 +38,12 @@ import {
|
|||||||
import { readJsonFile } from './utils.js';
|
import { readJsonFile } from './utils.js';
|
||||||
|
|
||||||
let db: Database;
|
let db: Database;
|
||||||
|
type RoomModeSource = 'explicit' | 'inferred';
|
||||||
|
|
||||||
|
interface StoredRoomModeRow {
|
||||||
|
roomMode: RoomMode;
|
||||||
|
source: RoomModeSource;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkItem {
|
export interface WorkItem {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -303,6 +309,7 @@ function createSchema(database: Database): void {
|
|||||||
CREATE TABLE IF NOT EXISTS room_settings (
|
CREATE TABLE IF NOT EXISTS room_settings (
|
||||||
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',
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
CHECK (room_mode IN ('single', 'tribunal'))
|
CHECK (room_mode IN ('single', 'tribunal'))
|
||||||
);
|
);
|
||||||
@@ -387,6 +394,20 @@ function createSchema(database: Database): void {
|
|||||||
/* column already exists */
|
/* column already exists */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
database.exec(
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN mode_source TEXT NOT NULL DEFAULT 'explicit'`,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
/* column already exists */
|
||||||
|
}
|
||||||
|
|
||||||
|
database.exec(
|
||||||
|
`UPDATE room_settings
|
||||||
|
SET mode_source = 'explicit'
|
||||||
|
WHERE COALESCE(mode_source, '') NOT IN ('explicit', 'inferred')`,
|
||||||
|
);
|
||||||
|
|
||||||
database.exec(`
|
database.exec(`
|
||||||
UPDATE scheduled_tasks
|
UPDATE scheduled_tasks
|
||||||
SET agent_type = COALESCE(
|
SET agent_type = COALESCE(
|
||||||
@@ -785,6 +806,8 @@ function createSchema(database: Database): void {
|
|||||||
} catch {
|
} catch {
|
||||||
/* columns already exist */
|
/* columns already exist */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backfillStoredRoomModes(database);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initDatabase(): void {
|
export function initDatabase(): void {
|
||||||
@@ -1832,6 +1855,7 @@ export function getRegisteredGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setRegisteredGroup(jid: string, group: RegisteredGroup): void {
|
export function setRegisteredGroup(jid: string, group: RegisteredGroup): void {
|
||||||
|
const existingRoomMode = getStoredRoomModeRow(jid);
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
@@ -1850,6 +1874,10 @@ export function setRegisteredGroup(jid: string, group: RegisteredGroup): void {
|
|||||||
group.agentType || 'claude-code',
|
group.agentType || 'claude-code',
|
||||||
group.workDir || null,
|
group.workDir || null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!existingRoomMode || existingRoomMode.source === 'inferred') {
|
||||||
|
syncInferredRoomModeForJid(jid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateRegisteredGroupName(jid: string, name: string): void {
|
export function updateRegisteredGroupName(jid: string, name: string): void {
|
||||||
@@ -1916,10 +1944,11 @@ export function getAllRegisteredGroups(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRegisteredAgentTypesForJid(jid: string): AgentType[] {
|
function collectRegisteredAgentTypes(
|
||||||
if (!db) return [];
|
database: Database,
|
||||||
|
jid: string,
|
||||||
const rows = db
|
): AgentType[] {
|
||||||
|
const rows = database
|
||||||
.prepare('SELECT agent_type FROM registered_groups WHERE jid = ?')
|
.prepare('SELECT agent_type FROM registered_groups WHERE jid = ?')
|
||||||
.all(jid) as Array<{ agent_type: string | null }>;
|
.all(jid) as Array<{ agent_type: string | null }>;
|
||||||
|
|
||||||
@@ -1933,6 +1962,11 @@ export function getRegisteredAgentTypesForJid(jid: string): AgentType[] {
|
|||||||
return [...types];
|
return [...types];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRegisteredAgentTypesForJid(jid: string): AgentType[] {
|
||||||
|
if (!db) return [];
|
||||||
|
return collectRegisteredAgentTypes(db, jid);
|
||||||
|
}
|
||||||
|
|
||||||
export function inferRoomModeFromRegisteredAgentTypes(
|
export function inferRoomModeFromRegisteredAgentTypes(
|
||||||
agentTypes: readonly AgentType[],
|
agentTypes: readonly AgentType[],
|
||||||
): RoomMode {
|
): RoomMode {
|
||||||
@@ -1946,37 +1980,75 @@ export function inferRoomModeForJid(jid: string): RoomMode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExplicitRoomMode(chatJid: string): RoomMode | undefined {
|
function normalizeRoomModeSource(
|
||||||
|
source: string | null | undefined,
|
||||||
|
): RoomModeSource | undefined {
|
||||||
|
return source === 'explicit' || source === 'inferred' ? source : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredRoomModeRow(chatJid: string): StoredRoomModeRow | undefined {
|
||||||
if (!db) return undefined;
|
if (!db) return undefined;
|
||||||
|
|
||||||
const row = db
|
const row = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT room_mode
|
`SELECT room_mode, mode_source
|
||||||
FROM room_settings
|
FROM room_settings
|
||||||
WHERE chat_jid = ?`,
|
WHERE chat_jid = ?`,
|
||||||
)
|
)
|
||||||
.get(chatJid) as { room_mode: string | null } | undefined;
|
.get(chatJid) as
|
||||||
return row?.room_mode === 'single' || row?.room_mode === 'tribunal'
|
| { room_mode: string | null; mode_source: string | null }
|
||||||
? row.room_mode
|
| undefined;
|
||||||
: 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setExplicitRoomMode(chatJid: string, roomMode: RoomMode): void {
|
function upsertStoredRoomMode(
|
||||||
|
chatJid: string,
|
||||||
|
roomMode: RoomMode,
|
||||||
|
source: RoomModeSource,
|
||||||
|
): void {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT OR REPLACE INTO room_settings (
|
`INSERT OR REPLACE INTO room_settings (
|
||||||
chat_jid,
|
chat_jid,
|
||||||
room_mode,
|
room_mode,
|
||||||
|
mode_source,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (?, ?, ?)`,
|
) VALUES (?, ?, ?, ?)`,
|
||||||
).run(chatJid, roomMode, new Date().toISOString());
|
).run(chatJid, roomMode, source, new Date().toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncInferredRoomModeForJid(chatJid: string): void {
|
||||||
|
upsertStoredRoomMode(chatJid, inferRoomModeForJid(chatJid), 'inferred');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExplicitRoomMode(chatJid: string): RoomMode | undefined {
|
||||||
|
const row = getStoredRoomModeRow(chatJid);
|
||||||
|
return row?.source === 'explicit' ? row.roomMode : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setExplicitRoomMode(chatJid: string, roomMode: RoomMode): void {
|
||||||
|
upsertStoredRoomMode(chatJid, roomMode, 'explicit');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearExplicitRoomMode(chatJid: string): void {
|
export function clearExplicitRoomMode(chatJid: string): void {
|
||||||
db.prepare('DELETE FROM room_settings WHERE chat_jid = ?').run(chatJid);
|
const agentTypes = getRegisteredAgentTypesForJid(chatJid);
|
||||||
|
if (agentTypes.length === 0) {
|
||||||
|
db.prepare('DELETE FROM room_settings WHERE chat_jid = ?').run(chatJid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
upsertStoredRoomMode(
|
||||||
|
chatJid,
|
||||||
|
inferRoomModeFromRegisteredAgentTypes(agentTypes),
|
||||||
|
'inferred',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEffectiveRoomMode(chatJid: string): RoomMode {
|
export function getEffectiveRoomMode(chatJid: string): RoomMode {
|
||||||
return getExplicitRoomMode(chatJid) ?? inferRoomModeForJid(chatJid);
|
return getStoredRoomModeRow(chatJid)?.roomMode ?? 'single';
|
||||||
}
|
}
|
||||||
|
|
||||||
function canRunTribunalFromRegisteredAgentTypes(
|
function canRunTribunalFromRegisteredAgentTypes(
|
||||||
@@ -2002,6 +2074,39 @@ export function isPairedRoomJid(jid: string): boolean {
|
|||||||
return getEffectiveRuntimeRoomMode(jid) === 'tribunal';
|
return getEffectiveRuntimeRoomMode(jid) === 'tribunal';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backfillStoredRoomModes(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`,
|
||||||
|
)
|
||||||
|
.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((missingRows: Array<{ jid: string }>) => {
|
||||||
|
const updatedAt = new Date().toISOString();
|
||||||
|
for (const row of missingRows) {
|
||||||
|
const roomMode = inferRoomModeFromRegisteredAgentTypes(
|
||||||
|
collectRegisteredAgentTypes(database, row.jid),
|
||||||
|
);
|
||||||
|
insertRoomMode.run(row.jid, roomMode, updatedAt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tx(rows);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Paired task/project/workspace state ---
|
// --- Paired task/project/workspace state ---
|
||||||
|
|
||||||
export function upsertPairedProject(project: PairedProject): void {
|
export function upsertPairedProject(project: PairedProject): void {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { getLastHumanMessageTimestamp, isPairedRoomJid } from './db.js';
|
import { getLastHumanMessageTimestamp } from './db.js';
|
||||||
import { filterProcessableMessages } from './bot-message-filter.js';
|
import { filterProcessableMessages } from './bot-message-filter.js';
|
||||||
import { normalizeStoredSeqCursor } from './message-cursor.js';
|
import { normalizeStoredSeqCursor } from './message-cursor.js';
|
||||||
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
||||||
import { isTaskStatusControlMessage } from './task-watch-status.js';
|
import { isTaskStatusControlMessage } from './task-watch-status.js';
|
||||||
import { ARBITER_AGENT_TYPE, REVIEWER_AGENT_TYPE } from './config.js';
|
import { ARBITER_AGENT_TYPE, REVIEWER_AGENT_TYPE } from './config.js';
|
||||||
|
import { hasReviewerLease } from './service-routing.js';
|
||||||
import {
|
import {
|
||||||
resolveAgentTypeForRole,
|
resolveAgentTypeForRole,
|
||||||
resolveRoleAgentPlan,
|
resolveRoleAgentPlan,
|
||||||
@@ -59,7 +60,7 @@ export function resolveCursorKey(
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
taskStatus?: string | null,
|
taskStatus?: string | null,
|
||||||
): string {
|
): string {
|
||||||
if (!isPairedRoomJid(chatJid)) return chatJid;
|
if (!hasReviewerLease(chatJid)) return chatJid;
|
||||||
const role = resolveActiveRole(taskStatus);
|
const role = resolveActiveRole(taskStatus);
|
||||||
return role === 'owner' ? chatJid : `${chatJid}:${role}`;
|
return role === 'owner' ? chatJid : `${chatJid}:${role}`;
|
||||||
}
|
}
|
||||||
@@ -128,7 +129,7 @@ export function shouldSkipBotOnlyCollaboration(
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
messages: NewMessage[],
|
messages: NewMessage[],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (isPairedRoomJid(chatJid)) return false;
|
if (hasReviewerLease(chatJid)) return false;
|
||||||
const allFromBots = messages.every(
|
const allFromBots = messages.every(
|
||||||
(message) => message.is_from_me || !!message.is_bot_message,
|
(message) => message.is_from_me || !!message.is_bot_message,
|
||||||
);
|
);
|
||||||
@@ -164,7 +165,7 @@ export function hasAllowedTrigger(opts: {
|
|||||||
|
|
||||||
// Paired rooms: bot-to-bot ping-pong doesn't need trigger patterns.
|
// Paired rooms: bot-to-bot ping-pong doesn't need trigger patterns.
|
||||||
// Peer bot messages (from reviewer/owner) are always allowed.
|
// Peer bot messages (from reviewer/owner) are always allowed.
|
||||||
if (isPairedRoomJid(chatJid)) {
|
if (hasReviewerLease(chatJid)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ export function getProcessableMessages(
|
|||||||
) {
|
) {
|
||||||
return filterProcessableMessages(
|
return filterProcessableMessages(
|
||||||
messages,
|
messages,
|
||||||
isPairedRoomJid(chatJid),
|
hasReviewerLease(chatJid),
|
||||||
channel?.isOwnMessage?.bind(channel),
|
channel?.isOwnMessage?.bind(channel),
|
||||||
).filter((message) => !isTaskStatusControlMessage(message.content));
|
).filter((message) => !isTaskStatusControlMessage(message.content));
|
||||||
}
|
}
|
||||||
@@ -195,7 +196,7 @@ export function filterLoopingPairedBotMessages(
|
|||||||
messages: Parameters<typeof filterProcessableMessages>[0],
|
messages: Parameters<typeof filterProcessableMessages>[0],
|
||||||
failureText: string,
|
failureText: string,
|
||||||
) {
|
) {
|
||||||
if (!isPairedRoomJid(chatJid)) return messages;
|
if (!hasReviewerLease(chatJid)) return messages;
|
||||||
|
|
||||||
return messages.filter(
|
return messages.filter(
|
||||||
(message) =>
|
(message) =>
|
||||||
|
|||||||
@@ -137,12 +137,12 @@ vi.mock('./db.js', () => {
|
|||||||
})),
|
})),
|
||||||
markWorkItemDelivered: vi.fn(),
|
markWorkItemDelivered: vi.fn(),
|
||||||
markWorkItemDeliveryRetry: vi.fn(),
|
markWorkItemDeliveryRetry: vi.fn(),
|
||||||
isPairedRoomJid: vi.fn(() => false),
|
|
||||||
getLastBotFinalMessage: vi.fn(() => []),
|
getLastBotFinalMessage: vi.fn(() => []),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('./service-routing.js', () => ({
|
vi.mock('./service-routing.js', () => ({
|
||||||
|
hasReviewerLease: vi.fn(() => false),
|
||||||
getEffectiveChannelLease: vi.fn((chatJid: string) => ({
|
getEffectiveChannelLease: vi.fn((chatJid: string) => ({
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
owner_service_id: 'claude',
|
owner_service_id: 'claude',
|
||||||
@@ -212,7 +212,7 @@ describe('createMessageRuntime', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
vi.mocked(db.getLastBotFinalMessage).mockReturnValue([]);
|
vi.mocked(db.getLastBotFinalMessage).mockReturnValue([]);
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(false);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(false);
|
||||||
vi.mocked(db.getRecentChatMessages).mockReturnValue([]);
|
vi.mocked(db.getRecentChatMessages).mockReturnValue([]);
|
||||||
vi.mocked(config.isClaudeService).mockReturnValue(true);
|
vi.mocked(config.isClaudeService).mockReturnValue(true);
|
||||||
vi.mocked(config.isReviewService).mockReturnValue(false);
|
vi.mocked(config.isReviewService).mockReturnValue(false);
|
||||||
@@ -225,7 +225,7 @@ describe('createMessageRuntime', () => {
|
|||||||
const saveState = vi.fn();
|
const saveState = vi.fn();
|
||||||
const lastAgentTimestamps: Record<string, string> = {};
|
const lastAgentTimestamps: Record<string, string> = {};
|
||||||
|
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(db.getMessagesSince).mockReturnValue([
|
vi.mocked(db.getMessagesSince).mockReturnValue([
|
||||||
{
|
{
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
@@ -280,7 +280,7 @@ describe('createMessageRuntime', () => {
|
|||||||
|
|
||||||
vi.mocked(config.isClaudeService).mockReturnValue(false);
|
vi.mocked(config.isClaudeService).mockReturnValue(false);
|
||||||
vi.mocked(config.isReviewService).mockReturnValue(false);
|
vi.mocked(config.isReviewService).mockReturnValue(false);
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(db.getMessagesSince).mockReturnValue([
|
vi.mocked(db.getMessagesSince).mockReturnValue([
|
||||||
{
|
{
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
@@ -355,7 +355,7 @@ describe('createMessageRuntime', () => {
|
|||||||
|
|
||||||
vi.mocked(config.isClaudeService).mockReturnValue(false);
|
vi.mocked(config.isClaudeService).mockReturnValue(false);
|
||||||
vi.mocked(config.isReviewService).mockReturnValue(true);
|
vi.mocked(config.isReviewService).mockReturnValue(true);
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(db.getMessagesSince).mockReturnValue([
|
vi.mocked(db.getMessagesSince).mockReturnValue([
|
||||||
{
|
{
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
@@ -423,7 +423,7 @@ describe('createMessageRuntime', () => {
|
|||||||
const saveState = vi.fn();
|
const saveState = vi.fn();
|
||||||
const lastAgentTimestamps: Record<string, string> = {};
|
const lastAgentTimestamps: Record<string, string> = {};
|
||||||
|
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(db.getMessagesSince).mockReturnValue([
|
vi.mocked(db.getMessagesSince).mockReturnValue([
|
||||||
{
|
{
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
@@ -553,7 +553,7 @@ describe('createMessageRuntime', () => {
|
|||||||
|
|
||||||
vi.mocked(config.isClaudeService).mockReturnValue(false);
|
vi.mocked(config.isClaudeService).mockReturnValue(false);
|
||||||
vi.mocked(config.isReviewService).mockReturnValue(false);
|
vi.mocked(config.isReviewService).mockReturnValue(false);
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue({
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue({
|
||||||
id: 'task-1',
|
id: 'task-1',
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
@@ -653,7 +653,7 @@ describe('createMessageRuntime', () => {
|
|||||||
const longReviewerOutput = `검토 요약 ${'a'.repeat(2105)} 끝`;
|
const longReviewerOutput = `검토 요약 ${'a'.repeat(2105)} 끝`;
|
||||||
const truncatedReviewerOutput = longReviewerOutput.slice(0, 2000);
|
const truncatedReviewerOutput = longReviewerOutput.slice(0, 2000);
|
||||||
|
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue({
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue({
|
||||||
id: 'task-merge-ready',
|
id: 'task-merge-ready',
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
@@ -756,7 +756,7 @@ describe('createMessageRuntime', () => {
|
|||||||
const group = makeGroup('codex');
|
const group = makeGroup('codex');
|
||||||
const channel = makeChannel(chatJid);
|
const channel = makeChannel(chatJid);
|
||||||
|
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue({
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue({
|
||||||
id: 'task-arbiter',
|
id: 'task-arbiter',
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
@@ -880,7 +880,7 @@ describe('createMessageRuntime', () => {
|
|||||||
isOwnMessage: vi.fn((msg) => msg.sender === 'shared-bot@test'),
|
isOwnMessage: vi.fn((msg) => msg.sender === 'shared-bot@test'),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
owner_service_id: 'claude',
|
owner_service_id: 'claude',
|
||||||
@@ -990,7 +990,7 @@ describe('createMessageRuntime', () => {
|
|||||||
isOwnMessage: vi.fn((msg) => msg.sender === 'shared-bot@test'),
|
isOwnMessage: vi.fn((msg) => msg.sender === 'shared-bot@test'),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
|
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
|
||||||
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
owner_service_id: 'claude',
|
owner_service_id: 'claude',
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
getLastBotFinalMessage,
|
getLastBotFinalMessage,
|
||||||
getLastHumanMessageContent,
|
getLastHumanMessageContent,
|
||||||
getRecentChatMessages,
|
getRecentChatMessages,
|
||||||
isPairedRoomJid,
|
|
||||||
getLatestOpenPairedTaskForChat,
|
getLatestOpenPairedTaskForChat,
|
||||||
getPairedTurnOutputs,
|
getPairedTurnOutputs,
|
||||||
updatePairedTask,
|
updatePairedTask,
|
||||||
@@ -65,7 +64,7 @@ import {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { resolveGroupIpcPath } from './group-folder.js';
|
import { resolveGroupIpcPath } from './group-folder.js';
|
||||||
import { getEffectiveChannelLease } from './service-routing.js';
|
import { getEffectiveChannelLease, hasReviewerLease } from './service-routing.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a message is a duplicate of the last bot final message in a paired room.
|
* Check if a message is a duplicate of the last bot final message in a paired room.
|
||||||
@@ -75,8 +74,7 @@ export function isDuplicateOfLastBotFinal(
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
text: string,
|
text: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
// Only check in paired rooms (both claude and codex registered)
|
if (!hasReviewerLease(chatJid)) {
|
||||||
if (!isPairedRoomJid(chatJid)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +128,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
messages: NewMessage[],
|
messages: NewMessage[],
|
||||||
): NewMessage[] => {
|
): NewMessage[] => {
|
||||||
if (!isPairedRoomJid(chatJid)) return messages;
|
if (!hasReviewerLease(chatJid)) return messages;
|
||||||
const lease = getEffectiveChannelLease(chatJid);
|
const lease = getEffectiveChannelLease(chatJid);
|
||||||
const sharedOwnerReviewerService =
|
const sharedOwnerReviewerService =
|
||||||
lease.reviewer_service_id !== null &&
|
lease.reviewer_service_id !== null &&
|
||||||
@@ -297,7 +295,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
messages: NewMessage[],
|
messages: NewMessage[],
|
||||||
): boolean =>
|
): boolean =>
|
||||||
isPairedRoomJid(chatJid) &&
|
hasReviewerLease(chatJid) &&
|
||||||
messages.every(
|
messages.every(
|
||||||
(message) => message.is_from_me === true || !!message.is_bot_message,
|
(message) => message.is_from_me === true || !!message.is_bot_message,
|
||||||
);
|
);
|
||||||
@@ -649,7 +647,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
// For paired rooms, determine the reviewer channel for correct bot routing.
|
// For paired rooms, determine the reviewer channel for correct bot routing.
|
||||||
// This is used whenever the reviewer needs to send output.
|
// This is used whenever the reviewer needs to send output.
|
||||||
const reviewerChannelName =
|
const reviewerChannelName =
|
||||||
isPairedRoomJid(chatJid) && REVIEWER_AGENT_TYPE === 'claude-code'
|
hasReviewerLease(chatJid) && REVIEWER_AGENT_TYPE === 'claude-code'
|
||||||
? 'discord'
|
? 'discord'
|
||||||
: 'discord-review';
|
: 'discord-review';
|
||||||
const foundReviewerChannel = findChannelByName(
|
const foundReviewerChannel = findChannelByName(
|
||||||
@@ -748,7 +746,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
return deliverySucceeded;
|
return deliverySucceeded;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isPairedRoomJid(chatJid)) {
|
if (hasReviewerLease(chatJid)) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
chatJid,
|
chatJid,
|
||||||
@@ -771,7 +769,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
(group.agentType || 'claude-code') as 'claude-code' | 'codex',
|
(group.agentType || 'claude-code') as 'claude-code' | 'codex',
|
||||||
);
|
);
|
||||||
if (openWorkItem) {
|
if (openWorkItem) {
|
||||||
const pendingTask = isPairedRoomJid(chatJid)
|
const pendingTask = hasReviewerLease(chatJid)
|
||||||
? getLatestOpenPairedTaskForChat(chatJid)
|
? getLatestOpenPairedTaskForChat(chatJid)
|
||||||
: null;
|
: null;
|
||||||
const deliveryChannel = resolveChannel(pendingTask?.status);
|
const deliveryChannel = resolveChannel(pendingTask?.status);
|
||||||
@@ -803,7 +801,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
if (missedMessages.length === 0) {
|
if (missedMessages.length === 0) {
|
||||||
// Check if a paired review is pending — run reviewer even without new messages
|
// Check if a paired review is pending — run reviewer even without new messages
|
||||||
const pendingReviewTask = isPairedRoomJid(chatJid)
|
const pendingReviewTask = hasReviewerLease(chatJid)
|
||||||
? getLatestOpenPairedTaskForChat(chatJid)
|
? getLatestOpenPairedTaskForChat(chatJid)
|
||||||
: null;
|
: null;
|
||||||
const pendingTurn = pendingReviewTask
|
const pendingTurn = pendingReviewTask
|
||||||
@@ -883,7 +881,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
resetPairedTask: () => {
|
resetPairedTask: () => {
|
||||||
if (isPairedRoomJid(chatJid)) {
|
if (hasReviewerLease(chatJid)) {
|
||||||
const task = getLatestOpenPairedTaskForChat(chatJid);
|
const task = getLatestOpenPairedTaskForChat(chatJid);
|
||||||
if (task) {
|
if (task) {
|
||||||
updatePairedTask(task.id, {
|
updatePairedTask(task.id, {
|
||||||
@@ -918,7 +916,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
// Determine role BEFORE advancing cursor — paired rooms use
|
// Determine role BEFORE advancing cursor — paired rooms use
|
||||||
// separate cursors for owner and reviewer so neither misses
|
// separate cursors for owner and reviewer so neither misses
|
||||||
// the other's messages.
|
// the other's messages.
|
||||||
const pendingTaskForChannel = isPairedRoomJid(chatJid)
|
const pendingTaskForChannel = hasReviewerLease(chatJid)
|
||||||
? getLatestOpenPairedTaskForChat(chatJid)
|
? getLatestOpenPairedTaskForChat(chatJid)
|
||||||
: null;
|
: null;
|
||||||
const taskStatus = pendingTaskForChannel?.status;
|
const taskStatus = pendingTaskForChannel?.status;
|
||||||
@@ -1130,7 +1128,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
// Use role-aware cursor for paired rooms so the reviewer
|
// Use role-aware cursor for paired rooms so the reviewer
|
||||||
// always sees the owner's last messages and vice versa.
|
// always sees the owner's last messages and vice versa.
|
||||||
const loopPendingTask = isPairedRoomJid(chatJid)
|
const loopPendingTask = hasReviewerLease(chatJid)
|
||||||
? getLatestOpenPairedTaskForChat(chatJid)
|
? getLatestOpenPairedTaskForChat(chatJid)
|
||||||
: null;
|
: null;
|
||||||
const loopCursorKey = resolveCursorKey(
|
const loopCursorKey = resolveCursorKey(
|
||||||
|
|||||||
@@ -171,6 +171,10 @@ export function isReviewerServiceForChat(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasReviewerLease(chatJid: string): boolean {
|
||||||
|
return getEffectiveChannelLease(chatJid).reviewer_service_id !== null;
|
||||||
|
}
|
||||||
|
|
||||||
export function isArbiterServiceForChat(
|
export function isArbiterServiceForChat(
|
||||||
chatJid: string,
|
chatJid: string,
|
||||||
serviceId: string = SERVICE_ID,
|
serviceId: string = SERVICE_ID,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
deleteTask,
|
deleteTask,
|
||||||
getDueTasks,
|
getDueTasks,
|
||||||
getTaskById,
|
getTaskById,
|
||||||
isPairedRoomJid,
|
|
||||||
logTaskRun,
|
logTaskRun,
|
||||||
updateTask,
|
updateTask,
|
||||||
updateTaskAfterRun,
|
updateTaskAfterRun,
|
||||||
@@ -62,6 +61,7 @@ import {
|
|||||||
shouldUseTaskScopedSession,
|
shouldUseTaskScopedSession,
|
||||||
} from './task-watch-status.js';
|
} from './task-watch-status.js';
|
||||||
import { AgentType, RegisteredGroup, ScheduledTask } from './types.js';
|
import { AgentType, RegisteredGroup, ScheduledTask } from './types.js';
|
||||||
|
import { hasReviewerLease } from './service-routing.js';
|
||||||
import {
|
import {
|
||||||
checkGitHubActionsRun,
|
checkGitHubActionsRun,
|
||||||
computeGitHubWatcherDelayMs,
|
computeGitHubWatcherDelayMs,
|
||||||
@@ -403,7 +403,7 @@ async function runTask(
|
|||||||
// In paired rooms, post cron output via reviewer bot so the
|
// In paired rooms, post cron output via reviewer bot so the
|
||||||
// owner treats it as a peer request and acts on it.
|
// owner treats it as a peer request and acts on it.
|
||||||
const send =
|
const send =
|
||||||
isPairedRoomJid(task.chat_jid) && deps.sendMessageViaReviewerBot
|
hasReviewerLease(task.chat_jid) && deps.sendMessageViaReviewerBot
|
||||||
? deps.sendMessageViaReviewerBot
|
? deps.sendMessageViaReviewerBot
|
||||||
: deps.sendMessage;
|
: deps.sendMessage;
|
||||||
await send(task.chat_jid, outputText);
|
await send(task.chat_jid, outputText);
|
||||||
@@ -757,7 +757,7 @@ async function runGithubCiTask(
|
|||||||
await statusTracker.update('completed');
|
await statusTracker.update('completed');
|
||||||
if (check.completionMessage) {
|
if (check.completionMessage) {
|
||||||
const send =
|
const send =
|
||||||
isPairedRoomJid(task.chat_jid) && deps.sendMessageViaReviewerBot
|
hasReviewerLease(task.chat_jid) && deps.sendMessageViaReviewerBot
|
||||||
? deps.sendMessageViaReviewerBot
|
? deps.sendMessageViaReviewerBot
|
||||||
: deps.sendMessage;
|
: deps.sendMessage;
|
||||||
await send(task.chat_jid, check.completionMessage);
|
await send(task.chat_jid, check.completionMessage);
|
||||||
|
|||||||
Reference in New Issue
Block a user