refactor(modules): extract scheduling as registry-based module

Moves the scheduling surface — 5 delivery actions (schedule_task,
cancel_task, pause_task, resume_task, update_task), handleRecurrence,
applyPreTaskScripts, and task DB helpers — out of core and into
src/modules/scheduling/ (host) and container/agent-runner/src/scheduling/
(container).

First PR to fill the MODULE-HOOK markers introduced in PR #2:
  - src/host-sweep.ts MODULE-HOOK:scheduling-recurrence now dynamically
    imports handleRecurrence from the module each sweep tick.
  - container/agent-runner/src/poll-loop.ts MODULE-HOOK:scheduling-pre-task
    dynamically imports applyPreTaskScripts before the provider call.
    When the marker block is empty (scheduling uninstalled), `keep`
    falls back to `normalMessages` so non-task messages still flow.

The 5 task cases are removed from delivery.ts's handleSystemAction
switch — the registry now routes them. Task DB helpers moved out of
src/db/session-db.ts (which kept `nextEvenSeq` as a named export so
the module can uphold the host-writes-even-seq invariant). Test suite
split to match: scheduling-specific tests live in the module.

No migration — tasks are messages_in rows with kind='task'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-18 16:17:47 +03:00
parent 71aab8c316
commit 473f766585
12 changed files with 657 additions and 517 deletions

View File

@@ -23,11 +23,6 @@ import {
markDelivered,
markDeliveryFailed,
migrateDeliveredTable,
insertTask,
cancelTask,
pauseTask,
resumeTask,
updateTask,
} from './db/session-db.js';
import { log } from './log.js';
import { normalizeOptions } from './channels/ask-question.js';
@@ -501,66 +496,6 @@ async function handleSystemAction(
}
switch (action) {
case 'schedule_task': {
const taskId = content.taskId as string;
const prompt = content.prompt as string;
const script = content.script as string | null;
const processAfter = content.processAfter as string;
const recurrence = (content.recurrence as string) || null;
insertTask(inDb, {
id: taskId,
processAfter,
recurrence,
platformId: (content.platformId as string) ?? null,
channelType: (content.channelType as string) ?? null,
threadId: (content.threadId as string) ?? null,
content: JSON.stringify({ prompt, script }),
});
log.info('Scheduled task created', { taskId, processAfter, recurrence });
break;
}
case 'cancel_task': {
const taskId = content.taskId as string;
cancelTask(inDb, taskId);
log.info('Task cancelled', { taskId });
break;
}
case 'pause_task': {
const taskId = content.taskId as string;
pauseTask(inDb, taskId);
log.info('Task paused', { taskId });
break;
}
case 'resume_task': {
const taskId = content.taskId as string;
resumeTask(inDb, taskId);
log.info('Task resumed', { taskId });
break;
}
case 'update_task': {
const taskId = content.taskId as string;
const update: Parameters<typeof updateTask>[2] = {};
if (typeof content.prompt === 'string') update.prompt = content.prompt;
if (typeof content.processAfter === 'string') update.processAfter = content.processAfter;
if (content.recurrence === null || typeof content.recurrence === 'string') {
update.recurrence = content.recurrence as string | null;
}
if (content.script === null || typeof content.script === 'string') {
update.script = content.script as string | null;
}
const touched = updateTask(inDb, taskId, update);
log.info('Task updated', { taskId, touched, fields: Object.keys(update) });
if (touched === 0) {
notifyAgent(session, `update_task: no live task matched id "${taskId}".`);
}
break;
}
case 'create_agent': {
const requestId = content.requestId as string;
const name = content.name as string;