From a6e1b8172c6faacaff74e4491271f78e9c44d517 Mon Sep 17 00:00:00 2001 From: ejclaw Date: Tue, 26 May 2026 04:17:59 +0900 Subject: [PATCH] fix: route scheduled watcher output by room role --- runners/agent-runner/src/ipc-mcp-stdio.ts | 29 ++++ src/db/bootstrap.test.ts | 1 + .../017_scheduled-task-room-role.ts | 13 ++ src/db/migrations/index.ts | 2 + src/db/tasks.ts | 14 +- src/ipc-auth.test.ts | 28 ++++ src/ipc-task-processor.ts | 21 ++- src/ipc-types.ts | 3 + src/task-scheduler-github-delivery.test.ts | 79 ++++++++++ src/task-scheduler-github.ts | 1 + src/task-scheduler-runtime.test.ts | 67 +++++++++ src/task-scheduler-runtime.ts | 5 +- src/task-scheduler.test.ts | 140 ------------------ src/task-scheduler.ts | 40 +++-- src/types.ts | 1 + 15 files changed, 288 insertions(+), 156 deletions(-) create mode 100644 src/db/migrations/017_scheduled-task-room-role.ts create mode 100644 src/task-scheduler-github-delivery.test.ts create mode 100644 src/task-scheduler-runtime.test.ts diff --git a/runners/agent-runner/src/ipc-mcp-stdio.ts b/runners/agent-runner/src/ipc-mcp-stdio.ts index 07c6ce4..ee23187 100644 --- a/runners/agent-runner/src/ipc-mcp-stdio.ts +++ b/runners/agent-runner/src/ipc-mcp-stdio.ts @@ -41,9 +41,22 @@ const chatJid = process.env.EJCLAW_CHAT_JID!; const groupFolder = process.env.EJCLAW_GROUP_FOLDER!; const isMain = process.env.EJCLAW_IS_MAIN === '1'; const agentType = process.env.EJCLAW_AGENT_TYPE || 'claude-code'; +const roomRole = process.env.EJCLAW_ROOM_ROLE; const runtimeTaskId = process.env.EJCLAW_RUNTIME_TASK_ID; const allowGenericScheduling = agentType !== 'codex'; +function currentAgentType(): 'claude-code' | 'codex' { + return agentType === 'codex' ? 'codex' : 'claude-code'; +} + +function currentRoomRole(): 'owner' | 'reviewer' | 'arbiter' | undefined { + return roomRole === 'owner' || + roomRole === 'reviewer' || + roomRole === 'arbiter' + ? roomRole + : undefined; +} + function writeIpcFile(dir: string, data: object): string { fs.mkdirSync(dir, { recursive: true }); @@ -214,6 +227,8 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone): prompt: args.prompt, schedule_type: args.schedule_type, schedule_value: args.schedule_value, + agent_type: currentAgentType(), + room_role: currentRoomRole(), context_mode: args.context_mode || 'group', targetJid, createdBy: groupFolder, @@ -301,6 +316,18 @@ server.tool( ), }, async (args) => { + if (roomRole && roomRole !== 'owner') { + return { + content: [ + { + type: 'text' as const, + text: 'watch_ci is owner-only in paired rooms. Ask the owner to schedule the watcher.', + }, + ], + isError: true, + }; + } + const isGitHubWatcher = args.ci_provider === 'github'; const target = args.target || @@ -383,6 +410,8 @@ server.tool( prompt, schedule_type: 'interval' as const, schedule_value: String(pollSeconds * 1000), + agent_type: currentAgentType(), + room_role: currentRoomRole(), context_mode: args.context_mode || DEFAULT_WATCH_CI_CONTEXT_MODE, ci_provider: args.ci_provider, ci_metadata: isGitHubWatcher diff --git a/src/db/bootstrap.test.ts b/src/db/bootstrap.test.ts index 6015b98..c1caac2 100644 --- a/src/db/bootstrap.test.ts +++ b/src/db/bootstrap.test.ts @@ -41,6 +41,7 @@ function getExpectedSchemaMigrations(): Array<{ { version: 14, name: 'work_item_attachments' }, { version: 15, name: 'turn_progress_text' }, { version: 16, name: 'room_skill_overrides' }, + { version: 17, name: 'scheduled_task_room_role' }, ]; } diff --git a/src/db/migrations/017_scheduled-task-room-role.ts b/src/db/migrations/017_scheduled-task-room-role.ts new file mode 100644 index 0000000..2a97c47 --- /dev/null +++ b/src/db/migrations/017_scheduled-task-room-role.ts @@ -0,0 +1,13 @@ +import type { SchemaMigrationDefinition } from './types.js'; +import { tryExecMigration } from './helpers.js'; + +export const SCHEDULED_TASK_ROOM_ROLE_MIGRATION = { + version: 17, + name: 'scheduled_task_room_role', + apply(database) { + tryExecMigration( + database, + `ALTER TABLE scheduled_tasks ADD COLUMN room_role TEXT`, + ); + }, +} satisfies SchemaMigrationDefinition; diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index 6ccb029..b7e93ca 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -16,6 +16,7 @@ import { MESSAGE_SOURCE_KIND_MIGRATION } from './013_message-source-kind.js'; import { WORK_ITEM_ATTACHMENTS_MIGRATION } from './014_work-item-attachments.js'; import { TURN_PROGRESS_TEXT_MIGRATION } from './015_turn-progress-text.js'; import { ROOM_SKILL_OVERRIDES_MIGRATION } from './016_room-skill-overrides.js'; +import { SCHEDULED_TASK_ROOM_ROLE_MIGRATION } from './017_scheduled-task-room-role.js'; import type { SchemaMigrationArgs, SchemaMigrationDefinition, @@ -40,6 +41,7 @@ const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [ WORK_ITEM_ATTACHMENTS_MIGRATION, TURN_PROGRESS_TEXT_MIGRATION, ROOM_SKILL_OVERRIDES_MIGRATION, + SCHEDULED_TASK_ROOM_ROLE_MIGRATION, ]; function ensureSchemaMigrationsTable(database: Database): void { diff --git a/src/db/tasks.ts b/src/db/tasks.ts index dd47070..3129e61 100644 --- a/src/db/tasks.ts +++ b/src/db/tasks.ts @@ -1,12 +1,18 @@ import { Database } from 'bun:sqlite'; -import { AgentType, ScheduledTask, TaskRunLog } from '../types.js'; +import { + AgentType, + PairedRoomRole, + ScheduledTask, + TaskRunLog, +} from '../types.js'; export type CreateScheduledTaskInput = Omit< ScheduledTask, | 'last_run' | 'last_result' | 'agent_type' + | 'room_role' | 'ci_provider' | 'ci_metadata' | 'max_duration_ms' @@ -14,6 +20,7 @@ export type CreateScheduledTaskInput = Omit< | 'status_started_at' > & { agent_type?: AgentType | null; + room_role?: PairedRoomRole | null; ci_provider?: ScheduledTask['ci_provider']; ci_metadata?: string | null; max_duration_ms?: number | null; @@ -45,8 +52,8 @@ export function createTaskInDatabase( database .prepare( ` - INSERT INTO scheduled_tasks (id, group_folder, chat_jid, agent_type, ci_provider, ci_metadata, max_duration_ms, status_message_id, status_started_at, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO scheduled_tasks (id, group_folder, chat_jid, agent_type, room_role, ci_provider, ci_metadata, max_duration_ms, status_message_id, status_started_at, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ) .run( @@ -54,6 +61,7 @@ export function createTaskInDatabase( task.group_folder, task.chat_jid, task.agent_type || 'claude-code', + task.room_role ?? null, task.ci_provider ?? null, task.ci_metadata ?? null, task.max_duration_ms ?? null, diff --git a/src/ipc-auth.test.ts b/src/ipc-auth.test.ts index 01b6ecc..f262f91 100644 --- a/src/ipc-auth.test.ts +++ b/src/ipc-auth.test.ts @@ -139,6 +139,34 @@ describe('schedule_task authorization', () => { expect(allTasks[0].agent_type).toBe('codex'); }); + it('stores the scheduling caller agent type when provided', async () => { + groups['other@g.us'] = { + ...OTHER_GROUP, + agentType: 'claude-code', + }; + _setRegisteredGroupForTests('other@g.us', groups['other@g.us']); + + await processTaskIpc( + { + type: 'schedule_task', + prompt: 'caller owned task', + schedule_type: 'once', + schedule_value: '2025-06-01T00:00:00', + targetJid: 'other@g.us', + agent_type: 'codex', + room_role: 'owner', + }, + 'whatsapp_main', + true, + deps, + ); + + const allTasks = getAllTasks(); + expect(allTasks).toHaveLength(1); + expect(allTasks[0].agent_type).toBe('codex'); + expect(allTasks[0].room_role).toBe('owner'); + }); + it('non-main group cannot schedule for another group', async () => { await processTaskIpc( { diff --git a/src/ipc-task-processor.ts b/src/ipc-task-processor.ts index 49670b2..7ec281d 100644 --- a/src/ipc-task-processor.ts +++ b/src/ipc-task-processor.ts @@ -16,17 +16,27 @@ import { } from './db.js'; import { isValidGroupFolder } from './group-folder.js'; import { logger } from './logger.js'; +import { normalizeStoredAgentType } from './db/room-registration.js'; import { DEFAULT_WATCH_CI_MAX_DURATION_MS, isWatchCiTask, } from './task-watch-status.js'; import type { IpcDeps, TaskIpcPayload } from './ipc-types.js'; +import type { PairedRoomRole } from './types.js'; type RoomBindings = ReturnType; type ScheduleType = 'cron' | 'interval' | 'once'; type TaskUpdates = Parameters[1]; type MemorySourceKind = Parameters[0]['sourceKind']; +function normalizePairedRoomRole( + role: string | null | undefined, +): PairedRoomRole | undefined { + return role === 'owner' || role === 'reviewer' || role === 'arbiter' + ? role + : undefined; +} + export async function processTaskIpc( data: TaskIpcPayload, sourceGroup: string, @@ -173,11 +183,17 @@ function handleScheduleTask( data.context_mode === 'group' || data.context_mode === 'isolated' ? data.context_mode : 'isolated'; + const scheduledAgentType = + normalizeStoredAgentType(data.agent_type) ?? + targetGroupEntry.agentType ?? + 'claude-code'; + const scheduledRoomRole = normalizePairedRoomRole(data.room_role); createTask({ id: taskId, group_folder: targetFolder, chat_jid: resolvedTargetJid, - agent_type: targetGroupEntry.agentType || 'claude-code', + agent_type: scheduledAgentType, + room_role: scheduledRoomRole ?? null, ci_provider: data.ci_provider ?? null, ci_metadata: data.ci_metadata ?? null, max_duration_ms: isWatchCiTask({ prompt: data.prompt }) @@ -197,7 +213,8 @@ function handleScheduleTask( sourceGroup, targetFolder, contextMode, - agentType: targetGroupEntry.agentType || 'claude-code', + agentType: scheduledAgentType, + roomRole: scheduledRoomRole ?? null, }, 'Task created via IPC', ); diff --git a/src/ipc-types.ts b/src/ipc-types.ts index 5464fac..8dcf576 100644 --- a/src/ipc-types.ts +++ b/src/ipc-types.ts @@ -4,6 +4,7 @@ import type { AgentType, MessageSourceKind, OutboundAttachment, + PairedRoomRole, PairedTask, RegisteredGroup, RoomMode, @@ -120,6 +121,8 @@ export interface TaskIpcPayload { schedule_type?: string; schedule_value?: string; context_mode?: string; + agent_type?: AgentType; + room_role?: PairedRoomRole; ci_provider?: 'github'; ci_metadata?: string; groupFolder?: string; diff --git a/src/task-scheduler-github-delivery.test.ts b/src/task-scheduler-github-delivery.test.ts new file mode 100644 index 0000000..231eb08 --- /dev/null +++ b/src/task-scheduler-github-delivery.test.ts @@ -0,0 +1,79 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { checkGitHubActionsRunMock, sendScheduledMessageMock } = vi.hoisted( + () => ({ + checkGitHubActionsRunMock: vi.fn(), + sendScheduledMessageMock: vi.fn(async () => {}), + }), +); + +vi.mock('./github-ci.js', () => ({ + checkGitHubActionsRun: checkGitHubActionsRunMock, + computeGitHubWatcherDelayMs: vi.fn(() => 15_000), + MAX_GITHUB_CONSECUTIVE_ERRORS: 5, + parseGitHubCiMetadata: vi.fn((raw: string | null | undefined) => + raw ? JSON.parse(raw) : null, + ), + serializeGitHubCiMetadata: vi.fn((metadata: unknown) => + JSON.stringify(metadata), + ), +})); + +vi.mock('./task-scheduler-runtime.js', () => ({ + sendScheduledMessage: sendScheduledMessageMock, +})); + +vi.mock('./task-status-tracker.js', () => ({ + createTaskStatusTracker: () => ({ + update: vi.fn(async () => {}), + }), +})); + +import { _initTestDatabase, createTask, getTaskById } from './db.js'; +import { runGithubCiTask } from './task-scheduler-github.js'; + +describe('GitHub watcher delivery identity', () => { + beforeEach(() => { + _initTestDatabase(); + checkGitHubActionsRunMock.mockReset(); + sendScheduledMessageMock.mockClear(); + }); + + it('passes the scheduling agent type to completion delivery', async () => { + checkGitHubActionsRunMock.mockResolvedValueOnce({ + terminal: true, + resultSummary: '성공: owner/repo run 765432', + completionMessage: 'CI 완료: GitHub Actions run 765432\n판정: 성공', + }); + createTask({ + id: 'task-github-codex-paired-complete', + group_folder: 'paired-group', + chat_jid: 'paired@g.us', + agent_type: 'codex', + room_role: 'owner', + ci_provider: 'github', + ci_metadata: JSON.stringify({ repo: 'owner/repo', run_id: 765432 }), + prompt: '[BACKGROUND CI WATCH]\nGitHub Actions run 765432', + schedule_type: 'interval', + schedule_value: '15000', + context_mode: 'isolated', + next_run: new Date().toISOString(), + status: 'active', + created_at: '2026-02-22T00:00:00.000Z', + }); + + const task = getTaskById('task-github-codex-paired-complete'); + expect(task).toBeDefined(); + const deps = { sendMessage: vi.fn(async () => {}) } as any; + + await runGithubCiTask(task!, deps); + + expect(sendScheduledMessageMock).toHaveBeenCalledWith( + deps, + 'paired@g.us', + 'CI 완료: GitHub Actions run 765432\n판정: 성공', + 'owner', + ); + expect(getTaskById('task-github-codex-paired-complete')).toBeUndefined(); + }); +}); diff --git a/src/task-scheduler-github.ts b/src/task-scheduler-github.ts index a264db4..095ea7b 100644 --- a/src/task-scheduler-github.ts +++ b/src/task-scheduler-github.ts @@ -61,6 +61,7 @@ export async function runGithubCiTask( deps, task.chat_jid, check.completionMessage, + task.room_role, ); } deleteTask(task.id); diff --git a/src/task-scheduler-runtime.test.ts b/src/task-scheduler-runtime.test.ts new file mode 100644 index 0000000..57f45a1 --- /dev/null +++ b/src/task-scheduler-runtime.test.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('./service-routing.js', () => ({ + hasReviewerLease: vi.fn(() => false), +})); + +import * as serviceRouting from './service-routing.js'; +import { sendScheduledMessage } from './task-scheduler-runtime.js'; + +describe('scheduled message delivery identity', () => { + beforeEach(() => { + vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(false); + }); + + it('uses the default owner identity for owner output in paired rooms', async () => { + vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true); + const sendMessage = vi.fn(async () => {}); + const sendMessageViaReviewerBot = vi.fn(async () => {}); + + await sendScheduledMessage( + { sendMessage, sendMessageViaReviewerBot } as any, + 'paired@g.us', + 'owner watcher done', + 'owner', + ); + + expect(sendMessage).toHaveBeenCalledWith( + 'paired@g.us', + 'owner watcher done', + ); + expect(sendMessageViaReviewerBot).not.toHaveBeenCalled(); + }); + + it('uses the reviewer identity for claude output in paired rooms', async () => { + vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true); + const sendMessage = vi.fn(async () => {}); + const sendMessageViaReviewerBot = vi.fn(async () => {}); + + await sendScheduledMessage( + { sendMessage, sendMessageViaReviewerBot } as any, + 'paired@g.us', + 'reviewer watcher done', + 'reviewer', + ); + + expect(sendMessageViaReviewerBot).toHaveBeenCalledWith( + 'paired@g.us', + 'reviewer watcher done', + ); + expect(sendMessage).not.toHaveBeenCalled(); + }); + + it('fails closed for reviewer output when the reviewer bot is missing', async () => { + vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true); + const sendMessage = vi.fn(async () => {}); + + await expect( + sendScheduledMessage( + { sendMessage } as any, + 'paired@g.us', + 'reviewer watcher done', + 'reviewer', + ), + ).rejects.toThrow(/reviewer Discord bot/); + expect(sendMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/src/task-scheduler-runtime.ts b/src/task-scheduler-runtime.ts index c67e929..fd2d234 100644 --- a/src/task-scheduler-runtime.ts +++ b/src/task-scheduler-runtime.ts @@ -14,7 +14,7 @@ import { getTaskRuntimeTaskId, shouldUseTaskScopedSession, } from './task-watch-status.js'; -import type { AgentType, ScheduledTask } from './types.js'; +import type { AgentType, PairedRoomRole, ScheduledTask } from './types.js'; import type { SchedulerDependencies, TaskExecutionContext, @@ -49,8 +49,9 @@ export async function sendScheduledMessage( deps: SchedulerDependencies, chatJid: string, text: string, + senderRoomRole?: PairedRoomRole | null, ): Promise { - if (!hasReviewerLease(chatJid)) { + if (!hasReviewerLease(chatJid) || senderRoomRole !== 'reviewer') { await deps.sendMessage(chatJid, text); return; } diff --git a/src/task-scheduler.test.ts b/src/task-scheduler.test.ts index 65af563..169e6d3 100644 --- a/src/task-scheduler.test.ts +++ b/src/task-scheduler.test.ts @@ -448,146 +448,6 @@ describe('task scheduler', () => { expect(task?.next_run).not.toBeNull(); }); - it('uses the reviewer bot identity for paired-room scheduled output', async () => { - const dueAt = new Date(Date.now() - 60_000).toISOString(); - createTask({ - id: 'task-paired-reviewer-output', - group_folder: 'paired-group', - chat_jid: 'paired@g.us', - agent_type: 'codex', - prompt: 'paired task', - schedule_type: 'once', - schedule_value: dueAt, - context_mode: 'isolated', - next_run: dueAt, - status: 'active', - created_at: '2026-02-22T00:00:00.000Z', - }); - - vi.mocked(serviceRouting.hasReviewerLease).mockImplementation( - (jid) => jid === 'paired@g.us', - ); - (runAgentProcessMock as any).mockImplementationOnce( - async ( - _group: unknown, - _input: unknown, - _onProcess: unknown, - onOutput?: (output: Record) => Promise, - ) => { - await onOutput?.({ - status: 'success', - phase: 'final', - result: 'paired reviewer output', - }); - return { - status: 'success', - result: 'paired reviewer output', - }; - }, - ); - - const enqueueTask = vi.fn( - async (_groupJid: string, _taskId: string, fn: () => Promise) => { - await fn(); - }, - ); - const sendMessage = vi.fn(async () => {}); - const sendMessageViaReviewerBot = vi.fn(async () => {}); - - await runSchedulerTickOnce({ - serviceAgentType: 'codex', - roomBindings: () => ({ - 'paired@g.us': { - name: 'Paired', - folder: 'paired-group', - trigger: '@Codex', - added_at: '2026-02-22T00:00:00.000Z', - agentType: 'codex', - }, - }), - getSessions: () => ({}), - queue: { enqueueTask } as any, - onProcess: () => {}, - sendMessage, - sendMessageViaReviewerBot, - }); - - expect(runAgentProcessMock).toHaveBeenCalledTimes(1); - expect(sendMessageViaReviewerBot).toHaveBeenCalledTimes(1); - expect(sendMessageViaReviewerBot).toHaveBeenCalledWith( - 'paired@g.us', - 'paired reviewer output', - ); - expect(sendMessage).not.toHaveBeenCalled(); - }); - - it('fails closed for paired-room scheduled output when the reviewer bot is missing', async () => { - const dueAt = new Date(Date.now() - 60_000).toISOString(); - createTask({ - id: 'task-paired-reviewer-missing', - group_folder: 'paired-group', - chat_jid: 'paired@g.us', - agent_type: 'codex', - prompt: 'paired task', - schedule_type: 'once', - schedule_value: dueAt, - context_mode: 'isolated', - next_run: dueAt, - status: 'active', - created_at: '2026-02-22T00:00:00.000Z', - }); - - vi.mocked(serviceRouting.hasReviewerLease).mockImplementation( - (jid) => jid === 'paired@g.us', - ); - (runAgentProcessMock as any).mockImplementationOnce( - async ( - _group: unknown, - _input: unknown, - _onProcess: unknown, - onOutput?: (output: Record) => Promise, - ) => { - await onOutput?.({ - status: 'success', - phase: 'final', - result: 'paired reviewer output', - }); - return { - status: 'success', - result: 'paired reviewer output', - }; - }, - ); - - const enqueueTask = vi.fn( - async (_groupJid: string, _taskId: string, fn: () => Promise) => { - await fn(); - }, - ); - const sendMessage = vi.fn(async () => {}); - - await runSchedulerTickOnce({ - serviceAgentType: 'codex', - roomBindings: () => ({ - 'paired@g.us': { - name: 'Paired', - folder: 'paired-group', - trigger: '@Codex', - added_at: '2026-02-22T00:00:00.000Z', - agentType: 'codex', - }, - }), - getSessions: () => ({}), - queue: { enqueueTask } as any, - onProcess: () => {}, - sendMessage, - }); - - expect(runAgentProcessMock).toHaveBeenCalledTimes(1); - expect(sendMessage).not.toHaveBeenCalled(); - expect(getTaskById('task-paired-reviewer-missing')).toBeDefined(); - }); - it('keeps group-context tasks on the chat queue key', async () => { const dueAt = new Date(Date.now() - 60_000).toISOString(); createTask({ diff --git a/src/task-scheduler.ts b/src/task-scheduler.ts index 5371465..b79dedd 100644 --- a/src/task-scheduler.ts +++ b/src/task-scheduler.ts @@ -107,6 +107,28 @@ export function computeNextRun(task: ScheduledTask): string | null { return null; } +async function forwardScheduledOutput( + deps: SchedulerDependencies, + task: ScheduledTask, + outputText: string, + taskRoomRole: ScheduledTask['room_role'], +): Promise { + await sendScheduledMessage(deps, task.chat_jid, outputText, taskRoomRole); +} + +function writeCurrentTaskSnapshot( + context: TaskExecutionContext, + task: ScheduledTask, + taskAgentType: TaskExecutionContext['taskAgentType'], +): void { + writeTaskSnapshotForGroup( + taskAgentType, + task.group_folder, + context.isMain, + context.runtimeTaskId, + ); +} + async function runTask( task: ScheduledTask, deps: SchedulerDependencies, @@ -149,14 +171,10 @@ async function runTask( }); log.info('Running scheduled task'); + const taskAgentType = context.taskAgentType; // Update tasks snapshot for agent to read (filtered by group) - writeTaskSnapshotForGroup( - context.taskAgentType, - task.group_folder, - context.isMain, - context.runtimeTaskId, - ); + writeCurrentTaskSnapshot(context, task, taskAgentType); let result: string | null = null; let error: string | null; @@ -164,7 +182,7 @@ async function runTask( sendTrackedMessage: deps.sendTrackedMessage, editTrackedMessage: deps.editTrackedMessage, }); - const isClaudeAgent = context.taskAgentType === 'claude-code'; + const isClaudeAgent = taskAgentType === 'claude-code'; try { await statusTracker.update('checking'); @@ -233,8 +251,12 @@ async function runTask( if (outputText) { attemptResult = outputText; - // Paired-room scheduler output must use the reviewer bot slot. - await sendScheduledMessage(deps, task.chat_jid, outputText); + await forwardScheduledOutput( + deps, + task, + outputText, + task.room_role, + ); } if (streamedOutput.status === 'error') { diff --git a/src/types.ts b/src/types.ts index 20b1c19..b54cc39 100644 --- a/src/types.ts +++ b/src/types.ts @@ -220,6 +220,7 @@ export interface ScheduledTask { group_folder: string; chat_jid: string; agent_type: AgentType | null; + room_role?: PairedRoomRole | null; ci_provider?: 'github' | null; ci_metadata?: string | null; max_duration_ms?: number | null;