feat: integrate Memento MCP for shared memory across agents

- Add MEMENTO_MCP_SSE_URL/ACCESS_KEY/REMOTE_PATH to readEnvFile
- Merge .env vars into cleanEnv for runner process inheritance
- Claude Code runner: add memento-mcp via mcp-remote in mcpServers
- Codex runner: inject memento-mcp section into config.toml
- Various improvements: CI watch, task scheduler, DB, IPC auth
This commit is contained in:
Eyejoker
2026-03-20 00:12:43 +09:00
parent e7200a1ed8
commit 41afcb91cf
17 changed files with 572 additions and 59 deletions

View File

@@ -287,9 +287,7 @@ describe('agent-runner timeout behavior', () => {
expect(fs.writeFileSync).toHaveBeenCalledWith(
'/tmp/ejclaw-test-data/sessions/eyejokerdb-4/.codex/config.toml',
expect.stringContaining(
'NANOCLAW_CHAT_JID = "dc:1481348008183595170"',
),
expect.stringContaining('NANOCLAW_CHAT_JID = "dc:1481348008183595170"'),
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
'/tmp/ejclaw-test-data/sessions/eyejokerdb-4/.codex/config.toml',

View File

@@ -175,10 +175,17 @@ function prepareGroupEnvironment(
'CODEX_OPENAI_API_KEY',
'CODEX_MODEL',
'CODEX_EFFORT',
'MEMENTO_MCP_SSE_URL',
'MEMENTO_ACCESS_KEY',
'MEMENTO_MCP_REMOTE_PATH',
]);
// Build a clean env without Claude Code nesting detection variables
const cleanEnv = { ...(process.env as Record<string, string>) };
// Merge .env file values (readEnvFile only reads file, doesn't set process.env)
for (const [k, v] of Object.entries(envVars)) {
if (v && !cleanEnv[k]) cleanEnv[k] = v;
}
delete cleanEnv.CLAUDECODE;
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
@@ -327,7 +334,28 @@ NANOCLAW_CHAT_JID = ${JSON.stringify(chatJid)}
NANOCLAW_GROUP_FOLDER = ${JSON.stringify(group.folder)}
NANOCLAW_IS_MAIN = ${JSON.stringify(isMain ? '1' : '0')}
`;
toml = toml.trimEnd() + '\n' + mcpSection;
// Inject memento-mcp if MEMENTO_MCP_SSE_URL is set
const mementoSseUrl =
envVars.MEMENTO_MCP_SSE_URL || process.env.MEMENTO_MCP_SSE_URL;
const mementoAccessKey =
envVars.MEMENTO_ACCESS_KEY || process.env.MEMENTO_ACCESS_KEY || '';
const mementoRemotePath =
envVars.MEMENTO_MCP_REMOTE_PATH ||
process.env.MEMENTO_MCP_REMOTE_PATH ||
'mcp-remote';
toml = toml.replace(
/\n?\[mcp_servers\.memento-mcp\][\s\S]*?(?=\n\[|$)/,
'',
);
const mementoSection = mementoSseUrl
? `
[mcp_servers.memento-mcp]
command = ${JSON.stringify(mementoRemotePath)}
args = [${JSON.stringify(mementoSseUrl)}, "--header", ${JSON.stringify(`Authorization:Bearer ${mementoAccessKey}`)}]
`
: '';
toml = toml.trimEnd() + '\n' + mcpSection + mementoSection;
fs.writeFileSync(configTomlPath, toml);
}

View File

@@ -7,6 +7,7 @@ import {
deleteTask,
getAllChats,
getAllRegisteredGroups,
getDueTasks,
getRegisteredAgentTypesForJid,
getMessagesSince,
getNewMessages,
@@ -402,6 +403,43 @@ describe('task CRUD', () => {
deleteTask('task-3');
expect(getTaskById('task-3')).toBeUndefined();
});
it('returns due tasks only for the requested agent type', () => {
const dueAt = new Date(Date.now() - 1_000).toISOString();
createTask({
id: 'task-claude',
group_folder: 'main',
chat_jid: 'group@g.us',
prompt: 'claude task',
schedule_type: 'once',
schedule_value: dueAt,
context_mode: 'isolated',
next_run: dueAt,
status: 'active',
created_at: '2024-01-01T00:00:00.000Z',
});
createTask({
id: 'task-codex',
group_folder: 'main',
chat_jid: 'group@g.us',
agent_type: 'codex',
prompt: 'codex task',
schedule_type: 'once',
schedule_value: dueAt,
context_mode: 'isolated',
next_run: dueAt,
status: 'active',
created_at: '2024-01-01T00:00:01.000Z',
});
expect(getDueTasks('claude-code').map((task) => task.id)).toEqual([
'task-claude',
]);
expect(getDueTasks('codex').map((task) => task.id)).toEqual([
'task-codex',
]);
});
});
// --- LIMIT behavior ---

117
src/db.ts
View File

@@ -48,6 +48,9 @@ function createSchema(database: Database.Database): void {
id TEXT PRIMARY KEY,
group_folder TEXT NOT NULL,
chat_jid TEXT NOT NULL,
agent_type TEXT,
status_message_id TEXT,
status_started_at TEXT,
prompt TEXT NOT NULL,
schedule_type TEXT NOT NULL,
schedule_value TEXT NOT NULL,
@@ -107,6 +110,47 @@ function createSchema(database: Database.Database): void {
/* column already exists */
}
try {
database.exec(`ALTER TABLE scheduled_tasks ADD COLUMN agent_type TEXT`);
} catch {
/* column already exists */
}
try {
database.exec(`ALTER TABLE scheduled_tasks ADD COLUMN status_message_id TEXT`);
} catch {
/* column already exists */
}
try {
database.exec(`ALTER TABLE scheduled_tasks ADD COLUMN status_started_at TEXT`);
} catch {
/* column already exists */
}
database.exec(`
UPDATE scheduled_tasks
SET agent_type = COALESCE(
(
SELECT CASE WHEN COUNT(*) = 1 THEN MIN(agent_type) ELSE NULL END
FROM registered_groups
WHERE jid = scheduled_tasks.chat_jid
AND folder = scheduled_tasks.group_folder
),
(
SELECT CASE WHEN COUNT(*) = 1 THEN MIN(agent_type) ELSE NULL END
FROM registered_groups
WHERE jid = scheduled_tasks.chat_jid
),
(
SELECT CASE WHEN COUNT(*) = 1 THEN MIN(agent_type) ELSE NULL END
FROM registered_groups
WHERE folder = scheduled_tasks.group_folder
)
)
WHERE agent_type IS NULL;
`);
// Add is_bot_message column if it doesn't exist (migration for existing DBs)
try {
database.exec(
@@ -420,17 +464,31 @@ export function getLastHumanMessageTimestamp(chatJid: string): string | null {
}
export function createTask(
task: Omit<ScheduledTask, 'last_run' | 'last_result'>,
task: Omit<
ScheduledTask,
| 'last_run'
| 'last_result'
| 'agent_type'
| 'status_message_id'
| 'status_started_at'
> & {
agent_type?: AgentType | null;
status_message_id?: string | null;
status_started_at?: string | null;
},
): void {
db.prepare(
`
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, agent_type, status_message_id, status_started_at, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
task.id,
task.group_folder,
task.chat_jid,
task.agent_type || SERVICE_AGENT_TYPE,
task.status_message_id || null,
task.status_started_at || null,
task.prompt,
task.schedule_type,
task.schedule_value,
@@ -447,7 +505,18 @@ export function getTaskById(id: string): ScheduledTask | undefined {
| undefined;
}
export function getTasksForGroup(groupFolder: string): ScheduledTask[] {
export function getTasksForGroup(
groupFolder: string,
agentType?: AgentType,
): ScheduledTask[] {
if (agentType) {
return db
.prepare(
'SELECT * FROM scheduled_tasks WHERE group_folder = ? AND agent_type = ? ORDER BY created_at DESC',
)
.all(groupFolder, agentType) as ScheduledTask[];
}
return db
.prepare(
'SELECT * FROM scheduled_tasks WHERE group_folder = ? ORDER BY created_at DESC',
@@ -455,7 +524,15 @@ export function getTasksForGroup(groupFolder: string): ScheduledTask[] {
.all(groupFolder) as ScheduledTask[];
}
export function getAllTasks(): ScheduledTask[] {
export function getAllTasks(agentType?: AgentType): ScheduledTask[] {
if (agentType) {
return db
.prepare(
'SELECT * FROM scheduled_tasks WHERE agent_type = ? ORDER BY created_at DESC',
)
.all(agentType) as ScheduledTask[];
}
return db
.prepare('SELECT * FROM scheduled_tasks ORDER BY created_at DESC')
.all() as ScheduledTask[];
@@ -502,23 +579,47 @@ export function updateTask(
).run(...values);
}
export function updateTaskStatusTracking(
id: string,
updates: Partial<Pick<ScheduledTask, 'status_message_id' | 'status_started_at'>>,
): void {
const fields: string[] = [];
const values: unknown[] = [];
if (updates.status_message_id !== undefined) {
fields.push('status_message_id = ?');
values.push(updates.status_message_id);
}
if (updates.status_started_at !== undefined) {
fields.push('status_started_at = ?');
values.push(updates.status_started_at);
}
if (fields.length === 0) return;
values.push(id);
db.prepare(
`UPDATE scheduled_tasks SET ${fields.join(', ')} WHERE id = ?`,
).run(...values);
}
export function deleteTask(id: string): void {
// Delete child records first (FK constraint)
db.prepare('DELETE FROM task_run_logs WHERE task_id = ?').run(id);
db.prepare('DELETE FROM scheduled_tasks WHERE id = ?').run(id);
}
export function getDueTasks(): ScheduledTask[] {
export function getDueTasks(agentType: AgentType = SERVICE_AGENT_TYPE): ScheduledTask[] {
const now = new Date().toISOString();
return db
.prepare(
`
SELECT * FROM scheduled_tasks
WHERE status = 'active' AND next_run IS NOT NULL AND next_run <= ?
WHERE status = 'active' AND agent_type = ? AND next_run IS NOT NULL AND next_run <= ?
ORDER BY next_run
`,
)
.all(now) as ScheduledTask[];
.all(agentType, now) as ScheduledTask[];
}
export function updateTaskAfterRun(

View File

@@ -218,6 +218,7 @@ async function main(): Promise<void> {
// Start subsystems (independently of connection handler)
startSchedulerLoop({
serviceAgentType: SERVICE_AGENT_TYPE,
registeredGroups: () => registeredGroups,
getSessions: () => sessions,
queue,
@@ -232,6 +233,28 @@ async function main(): Promise<void> {
const text = formatOutbound(rawText);
if (text) await channel.sendMessage(jid, text);
},
sendTrackedMessage: async (jid, rawText) => {
const channel = findChannel(channels, jid);
if (!channel?.sendAndTrack) {
return null;
}
const text = formatOutbound(rawText);
if (!text) {
return null;
}
return channel.sendAndTrack(jid, text);
},
editTrackedMessage: async (jid, messageId, rawText) => {
const channel = findChannel(channels, jid);
if (!channel?.editMessage) {
throw new Error(`Channel does not support message edits: ${jid}`);
}
const text = formatOutbound(rawText);
if (!text) {
throw new Error(`Cannot edit tracked message to empty text: ${jid}`);
}
await channel.editMessage(jid, messageId, text);
},
});
startIpcWatcher({
sendMessage: (jid, text) => {

View File

@@ -107,6 +107,31 @@ describe('schedule_task authorization', () => {
expect(allTasks[0].group_folder).toBe('other-group');
});
it('stores the target group agent type on scheduled tasks', async () => {
groups['other@g.us'] = {
...OTHER_GROUP,
agentType: 'codex',
};
setRegisteredGroup('other@g.us', groups['other@g.us']);
await processTaskIpc(
{
type: 'schedule_task',
prompt: 'codex owned task',
schedule_type: 'once',
schedule_value: '2025-06-01T00:00:00',
targetJid: 'other@g.us',
},
'whatsapp_main',
true,
deps,
);
const allTasks = getAllTasks();
expect(allTasks).toHaveLength(1);
expect(allTasks[0].agent_type).toBe('codex');
});
it('non-main group cannot schedule for another group', async () => {
await processTaskIpc(
{

View File

@@ -3,7 +3,12 @@ import path from 'path';
import { CronExpressionParser } from 'cron-parser';
import { DATA_DIR, IPC_POLL_INTERVAL, TIMEZONE } from './config.js';
import {
DATA_DIR,
IPC_POLL_INTERVAL,
SERVICE_AGENT_TYPE,
TIMEZONE,
} from './config.js';
import { AvailableGroup } from './agent-runner.js';
import { createTask, deleteTask, getTaskById, updateTask } from './db.js';
import { isValidGroupFolder } from './group-folder.js';
@@ -257,6 +262,7 @@ export async function processTaskIpc(
id: taskId,
group_folder: targetFolder,
chat_jid: targetJid,
agent_type: targetGroupEntry.agentType || SERVICE_AGENT_TYPE,
prompt: data.prompt,
schedule_type: scheduleType,
schedule_value: data.schedule_value,
@@ -266,7 +272,13 @@ export async function processTaskIpc(
created_at: new Date().toISOString(),
});
logger.info(
{ taskId, sourceGroup, targetFolder, contextMode },
{
taskId,
sourceGroup,
targetFolder,
contextMode,
agentType: targetGroupEntry.agentType || SERVICE_AGENT_TYPE,
},
'Task created via IPC',
);
}

View File

@@ -23,6 +23,7 @@ import {
isSessionCommandControlMessage,
} from './session-commands.js';
import { shouldResetSessionOnAgentFailure } from './session-recovery.js';
import { isTaskStatusControlMessage } from './task-scheduler.js';
import { Channel, NewMessage, RegisteredGroup } from './types.js';
import { logger } from './logger.js';
@@ -79,6 +80,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
if (msg.is_bot_message && isSessionCommandControlMessage(msg.content)) {
return false;
}
if (msg.is_bot_message && isTaskStatusControlMessage(msg.content)) {
return false;
}
return true;
});
@@ -98,7 +102,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
const sessions = deps.getSessions();
const sessionId = sessions[group.folder];
const tasks = getAllTasks();
const tasks = getAllTasks(group.agentType || 'claude-code');
writeTasksSnapshot(
group.folder,
isMain,

View File

@@ -4,6 +4,9 @@ import { _initTestDatabase, createTask, getTaskById } from './db.js';
import {
_resetSchedulerLoopForTests,
computeNextRun,
extractWatchCiTarget,
isWatchCiTask,
renderWatchCiStatusMessage,
startSchedulerLoop,
} from './task-scheduler.js';
@@ -52,12 +55,99 @@ describe('task scheduler', () => {
expect(task?.status).toBe('paused');
});
it('only enqueues tasks owned by the current service agent type', async () => {
const dueAt = new Date(Date.now() - 60_000).toISOString();
createTask({
id: 'task-claude',
group_folder: 'shared-group',
chat_jid: 'shared@g.us',
prompt: 'claude task',
schedule_type: 'once',
schedule_value: dueAt,
context_mode: 'isolated',
next_run: dueAt,
status: 'active',
created_at: '2026-02-22T00:00:00.000Z',
});
createTask({
id: 'task-codex',
group_folder: 'shared-group',
chat_jid: 'shared@g.us',
agent_type: 'codex',
prompt: 'codex task',
schedule_type: 'once',
schedule_value: dueAt,
context_mode: 'isolated',
next_run: dueAt,
status: 'active',
created_at: '2026-02-22T00:00:01.000Z',
});
const enqueueTask = vi.fn();
startSchedulerLoop({
serviceAgentType: 'codex',
registeredGroups: () => ({
'shared@g.us': {
name: 'Shared',
folder: 'shared-group',
trigger: '@Codex',
added_at: '2026-02-22T00:00:00.000Z',
agentType: 'codex',
},
}),
getSessions: () => ({}),
queue: { enqueueTask } as any,
onProcess: () => {},
sendMessage: async () => {},
});
await vi.advanceTimersByTimeAsync(10);
expect(enqueueTask).toHaveBeenCalledTimes(1);
expect(enqueueTask.mock.calls[0][1]).toBe('task-codex');
});
it('renders watcher heartbeat messages with target and timing', () => {
const prompt = `
[BACKGROUND CI WATCH]
Watch target:
GitHub Actions run 123456
Task ID:
task-123
Check instructions:
Check the run.
`.trim();
expect(isWatchCiTask({ prompt } as any)).toBe(true);
expect(extractWatchCiTarget(prompt)).toBe('GitHub Actions run 123456');
const rendered = renderWatchCiStatusMessage({
task: { prompt },
phase: 'waiting',
checkedAt: '2026-03-19T07:02:10.000Z',
nextRun: '2026-03-19T07:04:10.000Z',
});
expect(rendered).toContain('CI 감시 중: GitHub Actions run 123456');
expect(rendered).toContain('- 상태: 대기 중');
expect(rendered).toContain('- 마지막 확인:');
expect(rendered).toContain('- 다음 확인:');
expect(rendered).not.toContain('2분 10초');
});
it('computeNextRun anchors interval tasks to scheduled time to prevent drift', () => {
const scheduledTime = new Date(Date.now() - 2000).toISOString(); // 2s ago
const task = {
id: 'drift-test',
group_folder: 'test',
chat_jid: 'test@g.us',
agent_type: 'claude-code' as const,
status_message_id: null,
status_started_at: null,
prompt: 'test',
schedule_type: 'interval' as const,
schedule_value: '60000', // 1 minute
@@ -82,6 +172,9 @@ describe('task scheduler', () => {
id: 'once-test',
group_folder: 'test',
chat_jid: 'test@g.us',
agent_type: 'claude-code' as const,
status_message_id: null,
status_started_at: null,
prompt: 'test',
schedule_type: 'once' as const,
schedule_value: '2026-01-01T00:00:00.000Z',
@@ -106,6 +199,9 @@ describe('task scheduler', () => {
id: 'skip-test',
group_folder: 'test',
chat_jid: 'test@g.us',
agent_type: 'claude-code' as const,
status_message_id: null,
status_started_at: null,
prompt: 'test',
schedule_type: 'interval' as const,
schedule_value: String(ms),

View File

@@ -2,7 +2,12 @@ import { ChildProcess } from 'child_process';
import { CronExpressionParser } from 'cron-parser';
import fs from 'fs';
import { ASSISTANT_NAME, SCHEDULER_POLL_INTERVAL, TIMEZONE } from './config.js';
import {
ASSISTANT_NAME,
SCHEDULER_POLL_INTERVAL,
SERVICE_AGENT_TYPE,
TIMEZONE,
} from './config.js';
import {
AgentOutput,
runAgentProcess,
@@ -13,13 +18,14 @@ import {
getDueTasks,
getTaskById,
logTaskRun,
updateTaskStatusTracking,
updateTask,
updateTaskAfterRun,
} from './db.js';
import { GroupQueue } from './group-queue.js';
import { resolveGroupFolderPath } from './group-folder.js';
import { logger } from './logger.js';
import { RegisteredGroup, ScheduledTask } from './types.js';
import { AgentType, RegisteredGroup, ScheduledTask } from './types.js';
/**
* Compute the next run time for a recurring task, anchored to the
@@ -63,6 +69,7 @@ export function computeNextRun(task: ScheduledTask): string | null {
}
export interface SchedulerDependencies {
serviceAgentType?: AgentType;
registeredGroups: () => Record<string, RegisteredGroup>;
getSessions: () => Record<string, string>;
queue: GroupQueue;
@@ -73,6 +80,70 @@ export interface SchedulerDependencies {
groupFolder: string,
) => void;
sendMessage: (jid: string, text: string) => Promise<void>;
sendTrackedMessage?: (jid: string, text: string) => Promise<string | null>;
editTrackedMessage?: (
jid: string,
messageId: string,
text: string,
) => Promise<void>;
}
type WatcherStatusPhase = 'checking' | 'waiting' | 'retrying' | 'completed';
const WATCH_CI_PREFIX = '[BACKGROUND CI WATCH]';
const TASK_STATUS_MESSAGE_PREFIX = '\u2063\u2063\u2063';
export function isWatchCiTask(task: Pick<ScheduledTask, 'prompt'>): boolean {
return task.prompt.startsWith(WATCH_CI_PREFIX);
}
export function isTaskStatusControlMessage(content: string): boolean {
return content.startsWith(TASK_STATUS_MESSAGE_PREFIX);
}
export function extractWatchCiTarget(prompt: string): string | null {
const match = prompt.match(/Watch target:\n([\s\S]*?)\n\nTask ID:/);
return match?.[1]?.trim() || null;
}
function formatTimeLabel(timestampIso: string): string {
return new Intl.DateTimeFormat('ko-KR', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: TIMEZONE,
}).format(new Date(timestampIso));
}
export function renderWatchCiStatusMessage(args: {
task: Pick<ScheduledTask, 'prompt'>;
phase: WatcherStatusPhase;
checkedAt: string;
nextRun?: string | null;
}): string {
const target = extractWatchCiTarget(args.task.prompt) || 'CI watcher';
const title =
args.phase === 'completed' ? `CI 감시 종료: ${target}` : `CI 감시 중: ${target}`;
const statusLabel =
args.phase === 'checking'
? '확인 중'
: args.phase === 'retrying'
? '재시도 대기'
: args.phase === 'completed'
? '완료'
: '대기 중';
const lines = [
title,
`- 상태: ${statusLabel}`,
`- 마지막 확인: ${formatTimeLabel(args.checkedAt)}`,
];
if (args.nextRun) {
lines.push(`- 다음 확인: ${formatTimeLabel(args.nextRun)}`);
}
return lines.join('\n');
}
async function runTask(
@@ -131,7 +202,9 @@ async function runTask(
// Update tasks snapshot for agent to read (filtered by group)
const isMain = group.isMain === true;
const tasks = getAllTasks();
const taskAgentType =
task.agent_type || deps.serviceAgentType || SERVICE_AGENT_TYPE;
const tasks = getAllTasks(taskAgentType);
writeTasksSnapshot(
task.group_folder,
isMain,
@@ -148,6 +221,8 @@ async function runTask(
let result: string | null = null;
let error: string | null = null;
let statusMessageId = task.status_message_id;
let statusStartedAt = task.status_started_at;
// For group context mode, use the group's current session
const sessions = deps.getSessions();
@@ -168,7 +243,62 @@ async function runTask(
}, TASK_CLOSE_DELAY_MS);
};
const shouldTrackStatus =
isWatchCiTask(task) &&
typeof deps.sendTrackedMessage === 'function' &&
typeof deps.editTrackedMessage === 'function';
const persistStatusTracking = () => {
const currentTask = getTaskById(task.id);
if (!currentTask) return;
updateTaskStatusTracking(task.id, {
status_message_id: statusMessageId,
status_started_at: statusStartedAt,
});
};
const updateWatcherStatus = async (
phase: WatcherStatusPhase,
nextRun?: string | null,
) => {
if (!shouldTrackStatus) {
return;
}
const checkedAt = new Date().toISOString();
if (!statusStartedAt) {
statusStartedAt = checkedAt;
}
const text = renderWatchCiStatusMessage({
task,
phase,
checkedAt,
nextRun,
});
const payload = `${TASK_STATUS_MESSAGE_PREFIX}${text}`;
if (statusMessageId) {
try {
await deps.editTrackedMessage!(task.chat_jid, statusMessageId, payload);
persistStatusTracking();
return;
} catch {
statusMessageId = null;
persistStatusTracking();
}
}
const messageId = await deps.sendTrackedMessage!(task.chat_jid, payload);
if (messageId) {
statusMessageId = messageId;
persistStatusTracking();
}
};
try {
await updateWatcherStatus('checking');
const output = await runAgentProcess(
group,
{
@@ -212,7 +342,11 @@ async function runTask(
}
logger.info(
{ taskId: task.id, durationMs: Date.now() - startTime },
{
taskId: task.id,
agentType: taskAgentType,
durationMs: Date.now() - startTime,
},
'Task completed',
);
} catch (err) {
@@ -222,6 +356,22 @@ async function runTask(
}
const durationMs = Date.now() - startTime;
const currentTask = getTaskById(task.id);
const nextRun = currentTask ? computeNextRun(task) : null;
if (!currentTask) {
await updateWatcherStatus('completed');
logger.debug({ taskId: task.id }, 'Task deleted during execution, skipping persistence');
return;
}
if (error) {
await updateWatcherStatus('retrying', nextRun);
} else if (nextRun) {
await updateWatcherStatus('waiting', nextRun);
} else {
await updateWatcherStatus('completed');
}
logTaskRun({
task_id: task.id,
@@ -232,7 +382,6 @@ async function runTask(
error,
});
const nextRun = computeNextRun(task);
const resultSummary = error
? `Error: ${error}`
: result
@@ -253,7 +402,9 @@ export function startSchedulerLoop(deps: SchedulerDependencies): void {
const loop = async () => {
try {
const dueTasks = getDueTasks();
const dueTasks = getDueTasks(
deps.serviceAgentType || SERVICE_AGENT_TYPE,
);
if (dueTasks.length > 0) {
logger.info({ count: dueTasks.length }, 'Found due tasks');
}

View File

@@ -43,6 +43,9 @@ export interface ScheduledTask {
id: string;
group_folder: string;
chat_jid: string;
agent_type: AgentType | null;
status_message_id: string | null;
status_started_at: string | null;
prompt: string;
schedule_type: 'cron' | 'interval' | 'once';
schedule_value: string;