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

@@ -0,0 +1,49 @@
/**
* Sweep hook for recurring tasks.
*
* Every sweep tick, find `messages_in` rows that are `completed` AND still
* have a `recurrence` cron expression. For each, compute the next run via
* cron-parser, insert a fresh pending row (copying series_id forward), then
* clear the recurrence on the original so it isn't re-cloned next tick.
*
* Called from `src/host-sweep.ts` inside `MODULE-HOOK:scheduling-recurrence`.
* When scheduling ships inline (current state through PR #7), the hook is a
* direct dynamic import. When scheduling moves to the modules branch in
* PR #8, the install skill re-fills the marker on install.
*/
import type Database from 'better-sqlite3';
import { log } from '../../log.js';
import type { Session } from '../../types.js';
import { clearRecurrence, getCompletedRecurring, insertRecurrence } from './db.js';
export 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,
sessionId: session.id,
});
} catch (err) {
log.error('Failed to compute next recurrence', {
messageId: msg.id,
recurrence: msg.recurrence,
err,
});
}
}
}