fix: align routine system with actual RoutineStore/Routine APIs to prevent CLI crash

The RoutineRunner and RoutineScheduler were written against a different
interface than what RoutineStore actually implements, causing TypeError
crashes as soon as any routine became due. This adds the missing
agentId/catchUpLimit fields to the Routine type and DB schema, adds
startRoutineExecution/completeRoutineExecution/cancelRoutineExecution
methods to RoutineStore, and fixes all property name mismatches
(lastExecutedAt→lastRunAt, trigger.cron→trigger.cronExpression,
policy value alignment) in the runner, scheduler, and tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-10 11:26:19 -07:00
parent d693e45f2b
commit fa4c9f8841
8 changed files with 1123 additions and 4 deletions

View File

@@ -397,12 +397,14 @@ CREATE TABLE IF NOT EXISTS plugins (
-- Routines table for recurring task automation
CREATE TABLE IF NOT EXISTS routines (
id TEXT PRIMARY KEY,
agentId TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
description TEXT,
triggerType TEXT NOT NULL,
triggerConfig TEXT NOT NULL,
catchUpPolicy TEXT NOT NULL DEFAULT 'run_one',
executionPolicy TEXT NOT NULL DEFAULT 'queue',
catchUpLimit INTEGER DEFAULT 5,
enabled INTEGER DEFAULT 1,
lastRunAt TEXT,
lastRunResult TEXT,
@@ -970,12 +972,14 @@ export class Database {
this.db.exec(`
CREATE TABLE IF NOT EXISTS routines (
id TEXT PRIMARY KEY,
agentId TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
description TEXT,
triggerType TEXT NOT NULL,
triggerConfig TEXT NOT NULL,
catchUpPolicy TEXT NOT NULL DEFAULT 'run_one',
executionPolicy TEXT NOT NULL DEFAULT 'queue',
catchUpLimit INTEGER DEFAULT 5,
enabled INTEGER DEFAULT 1,
lastRunAt TEXT,
lastRunResult TEXT,

View File

@@ -105,6 +105,7 @@ export class RoutineStore extends EventEmitter<RoutineStoreEvents> {
return {
id: row.id,
agentId: row.agentId || "",
name: row.name,
description: row.description || undefined,
trigger,
@@ -116,6 +117,7 @@ export class RoutineStore extends EventEmitter<RoutineStoreEvents> {
nextRunAt: row.nextRunAt || undefined,
runCount: row.runCount || 0,
runHistory: fromJson<RoutineExecutionResult[]>(row.runHistory) || [],
catchUpLimit: row.catchUpLimit ?? 5,
cronExpression: isCronTrigger(trigger) ? trigger.cronExpression : undefined,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
@@ -144,19 +146,21 @@ export class RoutineStore extends EventEmitter<RoutineStoreEvents> {
this.db.prepare(`
INSERT OR REPLACE INTO routines (
id, name, description, triggerType, triggerConfig,
catchUpPolicy, executionPolicy, enabled,
id, agentId, name, description, triggerType, triggerConfig,
catchUpPolicy, executionPolicy, catchUpLimit, enabled,
lastRunAt, lastRunResult, nextRunAt,
runCount, runHistory, createdAt, updatedAt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
routine.id,
routine.agentId,
routine.name,
routine.description ?? null,
trigger.type,
JSON.stringify(triggerConfig),
routine.catchUpPolicy,
routine.executionPolicy,
routine.catchUpLimit ?? 5,
routine.enabled ? 1 : 0,
routine.lastRunAt ?? null,
routine.lastRunResult ? JSON.stringify(routine.lastRunResult) : null,
@@ -244,6 +248,7 @@ export class RoutineStore extends EventEmitter<RoutineStoreEvents> {
const routine: Routine = {
id,
agentId: input.agentId,
name: input.name.trim(),
description: input.description?.trim() || undefined,
trigger: input.trigger,
@@ -377,6 +382,54 @@ export class RoutineStore extends EventEmitter<RoutineStoreEvents> {
});
}
/**
* Mark a routine execution as started (pre-run bookkeeping).
*/
async startRoutineExecution(
id: string,
meta: { triggeredAt: string; catchUpFrom?: string; invocationSource: string },
): Promise<void> {
await this.withRoutineLock(id, async () => {
const routine = await this.getRoutine(id);
routine.lastRunAt = meta.triggeredAt;
routine.updatedAt = new Date().toISOString();
this.upsertRoutine(routine);
});
}
/**
* Record the completion (success or failure) of a routine execution.
*/
async completeRoutineExecution(
id: string,
meta: { completedAt: string; success: boolean; resultJson?: Record<string, unknown>; error?: string },
): Promise<void> {
const routine = await this.getRoutine(id);
const result: RoutineExecutionResult = {
routineId: id,
success: meta.success,
output: meta.success ? JSON.stringify(meta.resultJson ?? {}) : "",
error: meta.error,
startedAt: routine.lastRunAt ?? meta.completedAt,
completedAt: meta.completedAt,
};
await this.recordRun(id, result);
}
/**
* Cancel a routine execution (no result recorded, just reset state).
*/
async cancelRoutineExecution(id: string): Promise<void> {
await this.withRoutineLock(id, async () => {
const routine = await this.getRoutine(id);
if (routine.enabled && isCronTrigger(routine.trigger)) {
routine.nextRunAt = this.computeNextRun(routine.trigger.cronExpression);
}
routine.updatedAt = new Date().toISOString();
this.upsertRoutine(routine);
});
}
/**
* Get all routines that are due to run (nextRunAt <= now and enabled).
*/

View File

@@ -107,6 +107,8 @@ export interface RoutineExecutionResult extends AutomationRunResult {
export interface Routine {
/** Unique identifier (UUID). */
id: string;
/** ID of the agent that executes this routine. */
agentId: string;
/** Human-readable name. */
name: string;
/** Optional description of what this routine does. */
@@ -129,6 +131,8 @@ export interface Routine {
runCount: number;
/** History of recent run results (most recent first, capped at MAX_ROUTINE_RUN_HISTORY). */
runHistory: RoutineExecutionResult[];
/** Maximum number of catch-up executions when policy is "run". */
catchUpLimit?: number;
/** Optional cron expression stored directly for due-routine queries (derived from trigger). */
cronExpression?: string;
/** ISO-8601 timestamp of when this routine was created. */
@@ -143,6 +147,8 @@ export interface Routine {
export interface RoutineCreateInput {
/** Human-readable name. Required. */
name: string;
/** ID of the agent that executes this routine. Required. */
agentId: string;
/** Optional description. */
description?: string;
/** Trigger configuration. Required. */