fix: route scheduled watcher output by room role

This commit is contained in:
ejclaw
2026-05-26 04:17:59 +09:00
parent d770c40862
commit a6e1b8172c
15 changed files with 288 additions and 156 deletions

View File

@@ -41,9 +41,22 @@ const chatJid = process.env.EJCLAW_CHAT_JID!;
const groupFolder = process.env.EJCLAW_GROUP_FOLDER!; const groupFolder = process.env.EJCLAW_GROUP_FOLDER!;
const isMain = process.env.EJCLAW_IS_MAIN === '1'; const isMain = process.env.EJCLAW_IS_MAIN === '1';
const agentType = process.env.EJCLAW_AGENT_TYPE || 'claude-code'; 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 runtimeTaskId = process.env.EJCLAW_RUNTIME_TASK_ID;
const allowGenericScheduling = agentType !== 'codex'; 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 { function writeIpcFile(dir: string, data: object): string {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
@@ -214,6 +227,8 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
prompt: args.prompt, prompt: args.prompt,
schedule_type: args.schedule_type, schedule_type: args.schedule_type,
schedule_value: args.schedule_value, schedule_value: args.schedule_value,
agent_type: currentAgentType(),
room_role: currentRoomRole(),
context_mode: args.context_mode || 'group', context_mode: args.context_mode || 'group',
targetJid, targetJid,
createdBy: groupFolder, createdBy: groupFolder,
@@ -301,6 +316,18 @@ server.tool(
), ),
}, },
async (args) => { 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 isGitHubWatcher = args.ci_provider === 'github';
const target = const target =
args.target || args.target ||
@@ -383,6 +410,8 @@ server.tool(
prompt, prompt,
schedule_type: 'interval' as const, schedule_type: 'interval' as const,
schedule_value: String(pollSeconds * 1000), schedule_value: String(pollSeconds * 1000),
agent_type: currentAgentType(),
room_role: currentRoomRole(),
context_mode: args.context_mode || DEFAULT_WATCH_CI_CONTEXT_MODE, context_mode: args.context_mode || DEFAULT_WATCH_CI_CONTEXT_MODE,
ci_provider: args.ci_provider, ci_provider: args.ci_provider,
ci_metadata: isGitHubWatcher ci_metadata: isGitHubWatcher

View File

@@ -41,6 +41,7 @@ function getExpectedSchemaMigrations(): Array<{
{ version: 14, name: 'work_item_attachments' }, { version: 14, name: 'work_item_attachments' },
{ version: 15, name: 'turn_progress_text' }, { version: 15, name: 'turn_progress_text' },
{ version: 16, name: 'room_skill_overrides' }, { version: 16, name: 'room_skill_overrides' },
{ version: 17, name: 'scheduled_task_room_role' },
]; ];
} }

View File

@@ -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;

View File

@@ -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 { WORK_ITEM_ATTACHMENTS_MIGRATION } from './014_work-item-attachments.js';
import { TURN_PROGRESS_TEXT_MIGRATION } from './015_turn-progress-text.js'; import { TURN_PROGRESS_TEXT_MIGRATION } from './015_turn-progress-text.js';
import { ROOM_SKILL_OVERRIDES_MIGRATION } from './016_room-skill-overrides.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 { import type {
SchemaMigrationArgs, SchemaMigrationArgs,
SchemaMigrationDefinition, SchemaMigrationDefinition,
@@ -40,6 +41,7 @@ const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
WORK_ITEM_ATTACHMENTS_MIGRATION, WORK_ITEM_ATTACHMENTS_MIGRATION,
TURN_PROGRESS_TEXT_MIGRATION, TURN_PROGRESS_TEXT_MIGRATION,
ROOM_SKILL_OVERRIDES_MIGRATION, ROOM_SKILL_OVERRIDES_MIGRATION,
SCHEDULED_TASK_ROOM_ROLE_MIGRATION,
]; ];
function ensureSchemaMigrationsTable(database: Database): void { function ensureSchemaMigrationsTable(database: Database): void {

View File

@@ -1,12 +1,18 @@
import { Database } from 'bun:sqlite'; import { Database } from 'bun:sqlite';
import { AgentType, ScheduledTask, TaskRunLog } from '../types.js'; import {
AgentType,
PairedRoomRole,
ScheduledTask,
TaskRunLog,
} from '../types.js';
export type CreateScheduledTaskInput = Omit< export type CreateScheduledTaskInput = Omit<
ScheduledTask, ScheduledTask,
| 'last_run' | 'last_run'
| 'last_result' | 'last_result'
| 'agent_type' | 'agent_type'
| 'room_role'
| 'ci_provider' | 'ci_provider'
| 'ci_metadata' | 'ci_metadata'
| 'max_duration_ms' | 'max_duration_ms'
@@ -14,6 +20,7 @@ export type CreateScheduledTaskInput = Omit<
| 'status_started_at' | 'status_started_at'
> & { > & {
agent_type?: AgentType | null; agent_type?: AgentType | null;
room_role?: PairedRoomRole | null;
ci_provider?: ScheduledTask['ci_provider']; ci_provider?: ScheduledTask['ci_provider'];
ci_metadata?: string | null; ci_metadata?: string | null;
max_duration_ms?: number | null; max_duration_ms?: number | null;
@@ -45,8 +52,8 @@ export function createTaskInDatabase(
database database
.prepare( .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) 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, `,
) )
.run( .run(
@@ -54,6 +61,7 @@ export function createTaskInDatabase(
task.group_folder, task.group_folder,
task.chat_jid, task.chat_jid,
task.agent_type || 'claude-code', task.agent_type || 'claude-code',
task.room_role ?? null,
task.ci_provider ?? null, task.ci_provider ?? null,
task.ci_metadata ?? null, task.ci_metadata ?? null,
task.max_duration_ms ?? null, task.max_duration_ms ?? null,

View File

@@ -139,6 +139,34 @@ describe('schedule_task authorization', () => {
expect(allTasks[0].agent_type).toBe('codex'); 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 () => { it('non-main group cannot schedule for another group', async () => {
await processTaskIpc( await processTaskIpc(
{ {

View File

@@ -16,17 +16,27 @@ import {
} from './db.js'; } from './db.js';
import { isValidGroupFolder } from './group-folder.js'; import { isValidGroupFolder } from './group-folder.js';
import { logger } from './logger.js'; import { logger } from './logger.js';
import { normalizeStoredAgentType } from './db/room-registration.js';
import { import {
DEFAULT_WATCH_CI_MAX_DURATION_MS, DEFAULT_WATCH_CI_MAX_DURATION_MS,
isWatchCiTask, isWatchCiTask,
} from './task-watch-status.js'; } from './task-watch-status.js';
import type { IpcDeps, TaskIpcPayload } from './ipc-types.js'; import type { IpcDeps, TaskIpcPayload } from './ipc-types.js';
import type { PairedRoomRole } from './types.js';
type RoomBindings = ReturnType<IpcDeps['roomBindings']>; type RoomBindings = ReturnType<IpcDeps['roomBindings']>;
type ScheduleType = 'cron' | 'interval' | 'once'; type ScheduleType = 'cron' | 'interval' | 'once';
type TaskUpdates = Parameters<typeof updateTask>[1]; type TaskUpdates = Parameters<typeof updateTask>[1];
type MemorySourceKind = Parameters<typeof rememberMemory>[0]['sourceKind']; type MemorySourceKind = Parameters<typeof rememberMemory>[0]['sourceKind'];
function normalizePairedRoomRole(
role: string | null | undefined,
): PairedRoomRole | undefined {
return role === 'owner' || role === 'reviewer' || role === 'arbiter'
? role
: undefined;
}
export async function processTaskIpc( export async function processTaskIpc(
data: TaskIpcPayload, data: TaskIpcPayload,
sourceGroup: string, sourceGroup: string,
@@ -173,11 +183,17 @@ function handleScheduleTask(
data.context_mode === 'group' || data.context_mode === 'isolated' data.context_mode === 'group' || data.context_mode === 'isolated'
? data.context_mode ? data.context_mode
: 'isolated'; : 'isolated';
const scheduledAgentType =
normalizeStoredAgentType(data.agent_type) ??
targetGroupEntry.agentType ??
'claude-code';
const scheduledRoomRole = normalizePairedRoomRole(data.room_role);
createTask({ createTask({
id: taskId, id: taskId,
group_folder: targetFolder, group_folder: targetFolder,
chat_jid: resolvedTargetJid, chat_jid: resolvedTargetJid,
agent_type: targetGroupEntry.agentType || 'claude-code', agent_type: scheduledAgentType,
room_role: scheduledRoomRole ?? null,
ci_provider: data.ci_provider ?? null, ci_provider: data.ci_provider ?? null,
ci_metadata: data.ci_metadata ?? null, ci_metadata: data.ci_metadata ?? null,
max_duration_ms: isWatchCiTask({ prompt: data.prompt }) max_duration_ms: isWatchCiTask({ prompt: data.prompt })
@@ -197,7 +213,8 @@ function handleScheduleTask(
sourceGroup, sourceGroup,
targetFolder, targetFolder,
contextMode, contextMode,
agentType: targetGroupEntry.agentType || 'claude-code', agentType: scheduledAgentType,
roomRole: scheduledRoomRole ?? null,
}, },
'Task created via IPC', 'Task created via IPC',
); );

View File

@@ -4,6 +4,7 @@ import type {
AgentType, AgentType,
MessageSourceKind, MessageSourceKind,
OutboundAttachment, OutboundAttachment,
PairedRoomRole,
PairedTask, PairedTask,
RegisteredGroup, RegisteredGroup,
RoomMode, RoomMode,
@@ -120,6 +121,8 @@ export interface TaskIpcPayload {
schedule_type?: string; schedule_type?: string;
schedule_value?: string; schedule_value?: string;
context_mode?: string; context_mode?: string;
agent_type?: AgentType;
room_role?: PairedRoomRole;
ci_provider?: 'github'; ci_provider?: 'github';
ci_metadata?: string; ci_metadata?: string;
groupFolder?: string; groupFolder?: string;

View File

@@ -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();
});
});

View File

@@ -61,6 +61,7 @@ export async function runGithubCiTask(
deps, deps,
task.chat_jid, task.chat_jid,
check.completionMessage, check.completionMessage,
task.room_role,
); );
} }
deleteTask(task.id); deleteTask(task.id);

View File

@@ -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();
});
});

View File

@@ -14,7 +14,7 @@ import {
getTaskRuntimeTaskId, getTaskRuntimeTaskId,
shouldUseTaskScopedSession, shouldUseTaskScopedSession,
} from './task-watch-status.js'; } from './task-watch-status.js';
import type { AgentType, ScheduledTask } from './types.js'; import type { AgentType, PairedRoomRole, ScheduledTask } from './types.js';
import type { import type {
SchedulerDependencies, SchedulerDependencies,
TaskExecutionContext, TaskExecutionContext,
@@ -49,8 +49,9 @@ export async function sendScheduledMessage(
deps: SchedulerDependencies, deps: SchedulerDependencies,
chatJid: string, chatJid: string,
text: string, text: string,
senderRoomRole?: PairedRoomRole | null,
): Promise<void> { ): Promise<void> {
if (!hasReviewerLease(chatJid)) { if (!hasReviewerLease(chatJid) || senderRoomRole !== 'reviewer') {
await deps.sendMessage(chatJid, text); await deps.sendMessage(chatJid, text);
return; return;
} }

View File

@@ -448,146 +448,6 @@ describe('task scheduler', () => {
expect(task?.next_run).not.toBeNull(); 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<string, unknown>) => Promise<void>,
) => {
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<void>) => {
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<string, unknown>) => Promise<void>,
) => {
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<void>) => {
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 () => { it('keeps group-context tasks on the chat queue key', async () => {
const dueAt = new Date(Date.now() - 60_000).toISOString(); const dueAt = new Date(Date.now() - 60_000).toISOString();
createTask({ createTask({

View File

@@ -107,6 +107,28 @@ export function computeNextRun(task: ScheduledTask): string | null {
return null; return null;
} }
async function forwardScheduledOutput(
deps: SchedulerDependencies,
task: ScheduledTask,
outputText: string,
taskRoomRole: ScheduledTask['room_role'],
): Promise<void> {
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( async function runTask(
task: ScheduledTask, task: ScheduledTask,
deps: SchedulerDependencies, deps: SchedulerDependencies,
@@ -149,14 +171,10 @@ async function runTask(
}); });
log.info('Running scheduled task'); log.info('Running scheduled task');
const taskAgentType = context.taskAgentType;
// Update tasks snapshot for agent to read (filtered by group) // Update tasks snapshot for agent to read (filtered by group)
writeTaskSnapshotForGroup( writeCurrentTaskSnapshot(context, task, taskAgentType);
context.taskAgentType,
task.group_folder,
context.isMain,
context.runtimeTaskId,
);
let result: string | null = null; let result: string | null = null;
let error: string | null; let error: string | null;
@@ -164,7 +182,7 @@ async function runTask(
sendTrackedMessage: deps.sendTrackedMessage, sendTrackedMessage: deps.sendTrackedMessage,
editTrackedMessage: deps.editTrackedMessage, editTrackedMessage: deps.editTrackedMessage,
}); });
const isClaudeAgent = context.taskAgentType === 'claude-code'; const isClaudeAgent = taskAgentType === 'claude-code';
try { try {
await statusTracker.update('checking'); await statusTracker.update('checking');
@@ -233,8 +251,12 @@ async function runTask(
if (outputText) { if (outputText) {
attemptResult = outputText; attemptResult = outputText;
// Paired-room scheduler output must use the reviewer bot slot. await forwardScheduledOutput(
await sendScheduledMessage(deps, task.chat_jid, outputText); deps,
task,
outputText,
task.room_role,
);
} }
if (streamedOutput.status === 'error') { if (streamedOutput.status === 'error') {

View File

@@ -220,6 +220,7 @@ export interface ScheduledTask {
group_folder: string; group_folder: string;
chat_jid: string; chat_jid: string;
agent_type: AgentType | null; agent_type: AgentType | null;
room_role?: PairedRoomRole | null;
ci_provider?: 'github' | null; ci_provider?: 'github' | null;
ci_metadata?: string | null; ci_metadata?: string | null;
max_duration_ms?: number | null; max_duration_ms?: number | null;