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

@@ -19,9 +19,6 @@ import {
getMessageForRetry,
markMessageFailed,
retryWithBackoff,
getCompletedRecurring,
insertRecurrence,
clearRecurrence,
} from './db/session-db.js';
import { log } from './log.js';
import { openInboundDb, openOutboundDb, inboundDbPath, outboundDbPath, heartbeatPath } from './session-manager.js';
@@ -102,10 +99,7 @@ async function sweepSession(session: Session): Promise<void> {
// 4. Handle recurrence for completed messages.
// MODULE-HOOK:scheduling-recurrence:start
// When scheduling is extracted (PR #4), `handleRecurrence` moves to
// `src/modules/scheduling/` and the `/add-scheduling` skill replaces
// this block with a call to the module. Without scheduling
// installed, the block is empty and recurrence is a no-op.
const { handleRecurrence } = await import('./modules/scheduling/recurrence.js');
await handleRecurrence(inDb, session);
// MODULE-HOOK:scheduling-recurrence:end
} finally {
@@ -156,24 +150,3 @@ function detectStaleContainers(
}
}
/** Insert next occurrence for completed recurring messages. */
async function handleRecurrence(inDb: Database.Database, session: Session): Promise<void> {
const recurring = getCompletedRecurring(inDb);
for (const msg of recurring) {
try {
const { CronExpressionParser } = await import('cron-parser');
const interval = CronExpressionParser.parse(msg.recurrence);
const nextRun = interval.next().toISOString();
const prefix = msg.kind === 'task' ? 'task' : 'msg';
const newId = `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
insertRecurrence(inDb, msg, newId, nextRun);
clearRecurrence(inDb, msg.id);
log.info('Inserted next recurrence', { originalId: msg.id, newId, seriesId: msg.series_id, nextRun });
} catch (err) {
log.error('Failed to compute next recurrence', { messageId: msg.id, recurrence: msg.recurrence, err });
}
}
}