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:
@@ -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,
|
||||
|
||||
@@ -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).
|
||||
*/
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user