/** * Task DB helpers used by the scheduling module. * * Tasks are `messages_in` rows with `kind='task'`. This module doesn't own * its own table — it piggybacks on the core schema. That's why there's no * `module-scheduling-*.ts` migration file. * * cancel/pause/resume match any live row in the series, not just the exact id. * Recurring tasks get a new row per occurrence (see handleRecurrence), all * sharing series_id. Matching by id alone would only hit the completed row * the agent remembers, missing the live next occurrence. */ import type Database from 'better-sqlite3'; import { nextEvenSeq } from '../../db/session-db.js'; export function insertTask( db: Database.Database, task: { id: string; processAfter: string; recurrence: string | null; platformId: string | null; channelType: string | null; threadId: string | null; content: string; }, ): void { db.prepare( `INSERT INTO messages_in (id, seq, timestamp, status, tries, process_after, recurrence, kind, platform_id, channel_type, thread_id, content, series_id) VALUES (@id, @seq, datetime('now'), 'pending', 0, @processAfter, @recurrence, 'task', @platformId, @channelType, @threadId, @content, @id)`, ).run({ ...task, seq: nextEvenSeq(db), }); } export function cancelTask(db: Database.Database, taskId: string): void { db.prepare( "UPDATE messages_in SET status = 'completed', recurrence = NULL WHERE (id = ? OR series_id = ?) AND kind = 'task' AND status IN ('pending', 'paused')", ).run(taskId, taskId); } export function pauseTask(db: Database.Database, taskId: string): void { db.prepare( "UPDATE messages_in SET status = 'paused' WHERE (id = ? OR series_id = ?) AND kind = 'task' AND status = 'pending'", ).run(taskId, taskId); } export function resumeTask(db: Database.Database, taskId: string): void { db.prepare( "UPDATE messages_in SET status = 'pending' WHERE (id = ? OR series_id = ?) AND kind = 'task' AND status = 'paused'", ).run(taskId, taskId); } export interface TaskUpdate { prompt?: string; script?: string | null; recurrence?: string | null; processAfter?: string; } // Merges content JSON in-place so callers can update prompt/script without // clobbering other fields. Matches by id OR series_id so the live next // occurrence of a recurring task is updated, not just the completed row the // agent last saw. Returns the number of rows touched. export function updateTask(db: Database.Database, taskId: string, update: TaskUpdate): number { const rows = db .prepare( "SELECT id, content FROM messages_in WHERE (id = ? OR series_id = ?) AND kind = 'task' AND status IN ('pending', 'paused')", ) .all(taskId, taskId) as Array<{ id: string; content: string }>; if (rows.length === 0) return 0; const setProcessAfter = update.processAfter !== undefined; const setRecurrence = update.recurrence !== undefined; const mergeContent = update.prompt !== undefined || update.script !== undefined; const tx = db.transaction(() => { for (const row of rows) { let content = row.content; if (mergeContent) { const parsed = JSON.parse(row.content) as Record; if (update.prompt !== undefined) parsed.prompt = update.prompt; if (update.script !== undefined) parsed.script = update.script; content = JSON.stringify(parsed); } // Build SET clause dynamically so callers can update fields independently. const sets: string[] = ['content = ?']; const params: unknown[] = [content]; if (setProcessAfter) { sets.push('process_after = ?'); params.push(update.processAfter); } if (setRecurrence) { sets.push('recurrence = ?'); params.push(update.recurrence); } params.push(row.id); db.prepare(`UPDATE messages_in SET ${sets.join(', ')} WHERE id = ?`).run(...params); } }); tx(); return rows.length; } export interface RecurringMessage { id: string; kind: string; content: string; recurrence: string; process_after: string | null; platform_id: string | null; channel_type: string | null; thread_id: string | null; series_id: string; } export function getCompletedRecurring(db: Database.Database): RecurringMessage[] { return db .prepare("SELECT * FROM messages_in WHERE status = 'completed' AND recurrence IS NOT NULL") .all() as RecurringMessage[]; } export function insertRecurrence( db: Database.Database, msg: RecurringMessage, newId: string, nextRun: string | null, ): void { db.prepare( `INSERT INTO messages_in (id, seq, kind, timestamp, status, process_after, recurrence, platform_id, channel_type, thread_id, content, series_id) VALUES (?, ?, ?, datetime('now'), 'pending', ?, ?, ?, ?, ?, ?, ?)`, ).run( newId, nextEvenSeq(db), msg.kind, nextRun, msg.recurrence, msg.platform_id, msg.channel_type, msg.thread_id, msg.content, msg.series_id, ); } export function clearRecurrence(db: Database.Database, messageId: string): void { db.prepare('UPDATE messages_in SET recurrence = NULL WHERE id = ?').run(messageId); }