@johpaz/hive-sdk 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +280 -0
- package/docs/API-AGENTS.md +316 -0
- package/docs/API-CONTEXT-COMPILER.md +252 -0
- package/docs/API-DAG-SCHEDULER.md +273 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +350 -0
- package/docs/API-WORKERS-EVENTS.md +299 -0
- package/docs/INDEX.md +190 -0
- package/docs/README.md +161 -0
- package/docs/TEMPLATE-HIVE-APP.md +360 -0
- package/package.json +60 -104
- package/packages/cli/bin/hive +2 -0
- package/packages/cli/package.json +17 -0
- package/packages/cli/src/commands/add-skill.ts +42 -0
- package/packages/cli/src/commands/add-tool.ts +45 -0
- package/packages/cli/src/commands/add-worker.ts +49 -0
- package/packages/cli/src/commands/create-app-utils.ts +32 -0
- package/packages/cli/src/commands/create-app.test.ts +151 -0
- package/packages/cli/src/commands/create-app.ts +35 -0
- package/packages/cli/src/commands/init.ts +56 -0
- package/packages/cli/src/commands/run.ts +45 -0
- package/packages/cli/src/commands/test.ts +42 -0
- package/packages/cli/src/commands/trace.ts +55 -0
- package/packages/cli/src/index.ts +59 -0
- package/packages/cli/templates/hive-app/.env.example +17 -0
- package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
- package/packages/cli/templates/hive-app/hive.config.ts +19 -0
- package/packages/cli/templates/hive-app/package.json +16 -0
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
- package/packages/cli/templates/hive-app/src/main.ts +56 -0
- package/packages/core/package.json +58 -0
- package/packages/core/src/ace/Curator.ts +158 -0
- package/packages/core/src/ace/Reflector.ts +200 -0
- package/packages/core/src/ace/Tracer.ts +100 -0
- package/packages/core/src/ace/index.ts +4 -0
- package/packages/core/src/agent/AgentRunner.ts +699 -0
- package/packages/core/src/agent/Compaction.ts +221 -0
- package/packages/core/src/agent/ContextCompiler.ts +567 -0
- package/packages/core/src/agent/ContextGuard.ts +91 -0
- package/packages/core/src/agent/ConversationStore.ts +244 -0
- package/packages/core/src/agent/Hooks.ts +166 -0
- package/packages/core/src/agent/NativeTools.ts +31 -0
- package/packages/core/src/agent/PromptBuilder.ts +169 -0
- package/packages/core/src/agent/Service.ts +267 -0
- package/packages/core/src/agent/StuckLoop.ts +133 -0
- package/packages/core/src/agent/index.ts +12 -0
- package/packages/core/src/agent/providers/LLMClient.ts +149 -0
- package/packages/core/src/agent/providers/anthropic.ts +212 -0
- package/packages/core/src/agent/providers/gemini.ts +215 -0
- package/packages/core/src/agent/providers/index.ts +199 -0
- package/packages/core/src/agent/providers/interface.ts +195 -0
- package/packages/core/src/agent/providers/ollama.ts +175 -0
- package/packages/core/src/agent/providers/openai-compat.ts +231 -0
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
- package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
- package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
- package/packages/core/src/agent/selectors/index.ts +6 -0
- package/packages/core/src/api/createAgent.test.ts +48 -0
- package/packages/core/src/api/createAgent.ts +122 -0
- package/packages/core/src/api/index.ts +2 -0
- package/packages/core/src/auth/auth.ts +108 -0
- package/packages/core/src/auth/index.ts +1 -0
- package/packages/core/src/canvas/CanvasManager.ts +390 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/canvas.test.ts +32 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +3 -0
- package/packages/core/src/channels/base.ts +154 -0
- package/packages/core/src/channels/channels.test.ts +18 -0
- package/packages/core/src/channels/discord.ts +273 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +450 -0
- package/packages/core/src/channels/slack.ts +323 -0
- package/packages/core/src/channels/telegram.ts +612 -0
- package/packages/core/src/channels/webchat.ts +139 -0
- package/packages/core/src/channels/whatsapp.ts +548 -0
- package/packages/core/src/config/index.ts +2 -0
- package/packages/core/src/config/loader.ts +554 -0
- package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
- package/packages/core/src/ethics/EthicsGuard.ts +66 -0
- package/packages/core/src/ethics/index.ts +2 -0
- package/packages/core/src/events/agent-bus.ts +460 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/channel-notify.ts +37 -0
- package/packages/core/src/gateway/gateway.test.ts +38 -0
- package/packages/core/src/gateway/index.ts +2 -0
- package/packages/core/src/gateway/server.ts +139 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +81 -0
- package/packages/core/src/mcp/MCPClient.ts +439 -0
- package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
- package/packages/core/src/mcp/config.ts +13 -0
- package/packages/core/src/mcp/hot-reload.ts +147 -0
- package/packages/core/src/mcp/index.ts +11 -0
- package/packages/core/src/mcp/logger.ts +42 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/transports/index.ts +67 -0
- package/packages/core/src/mcp/transports/sse.ts +241 -0
- package/packages/core/src/mcp/transports/websocket.ts +159 -0
- package/packages/core/src/memory/Scratchpad.test.ts +47 -0
- package/packages/core/src/memory/Scratchpad.ts +37 -0
- package/packages/core/src/memory/Storage.ts +6 -0
- package/packages/core/src/memory/index.ts +2 -0
- package/packages/core/src/multimodal/VisionService.ts +293 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/multimodal/vision-service.ts +283 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/scheduler/CronScheduler.ts +699 -0
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
- package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
- package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
- package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
- package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
- package/packages/core/src/scheduler/dag/errors.ts +37 -0
- package/packages/core/src/scheduler/dag/index.ts +26 -0
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/scheduler/index.ts +22 -0
- package/packages/core/src/scheduler/integration.ts +237 -0
- package/packages/core/src/scheduler/scheduler.test.ts +19 -0
- package/packages/core/src/scheduler/types.ts +164 -0
- package/packages/core/src/security/Pairing.ts +250 -0
- package/packages/core/src/security/RateLimit.ts +270 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/skills/SkillLoader.ts +388 -0
- package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
- package/packages/core/src/skills/defineSkill.ts +18 -0
- package/packages/core/src/skills/index.ts +4 -0
- package/packages/core/src/state/index.ts +2 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/SQLiteStorage.ts +407 -0
- package/packages/core/src/storage/crypto.ts +233 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/onboarding.ts +1603 -0
- package/packages/core/src/storage/schema.ts +689 -0
- package/packages/core/src/storage/seed.ts +740 -0
- package/packages/core/src/storage/storage.test.ts +37 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/swarm/AgentBus.ts +460 -0
- package/packages/core/src/swarm/AgentExecutor.ts +53 -0
- package/packages/core/src/swarm/Coordinator.ts +251 -0
- package/packages/core/src/swarm/EventBridge.ts +122 -0
- package/packages/core/src/swarm/EventBus.ts +169 -0
- package/packages/core/src/swarm/TaskGraph.ts +192 -0
- package/packages/core/src/swarm/TaskNode.ts +97 -0
- package/packages/core/src/swarm/TaskResult.ts +22 -0
- package/packages/core/src/swarm/WorkerPool.ts +236 -0
- package/packages/core/src/swarm/errors.ts +37 -0
- package/packages/core/src/swarm/index.ts +30 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
- package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/swarm/presets/index.ts +4 -0
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/swarm/strategies/index.ts +3 -0
- package/packages/core/src/swarm/swarm.test.ts +24 -0
- package/packages/core/src/swarm/types.ts +164 -0
- package/packages/core/src/tool-runtime/index.ts +522 -0
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
- package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
- package/packages/core/src/tools/ToolExecutor.ts +58 -0
- package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
- package/packages/core/src/tools/ToolRegistry.ts +61 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +715 -0
- package/packages/core/src/tools/bridge-events.ts +26 -0
- package/packages/core/src/tools/canvas/index.ts +375 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/codebridge/index.ts +342 -0
- package/packages/core/src/tools/core/index.ts +476 -0
- package/packages/core/src/tools/cron/index.ts +626 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +231 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/projects/index.ts +37 -0
- package/packages/core/src/tools/projects/project-create.ts +94 -0
- package/packages/core/src/tools/projects/project-done.ts +66 -0
- package/packages/core/src/tools/projects/project-fail.ts +66 -0
- package/packages/core/src/tools/projects/project-list.ts +96 -0
- package/packages/core/src/tools/projects/project-update.ts +72 -0
- package/packages/core/src/tools/projects/task-create.ts +68 -0
- package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
- package/packages/core/src/tools/projects/task-update.ts +93 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +10 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +643 -0
- package/packages/core/src/workers/WorkerPool.ts +167 -0
- package/packages/core/src/workers/agent.worker.ts +68 -0
- package/packages/core/src/workers/createWorker.ts +144 -0
- package/packages/core/src/workers/index.ts +5 -0
- package/packages/core/src/workers/workers.test.ts +48 -0
- package/test/setup-db.ts +216 -0
- package/tsconfig.json +40 -0
- package/src/agents.ts +0 -1
- package/src/canvas.ts +0 -1
- package/src/channels.ts +0 -1
- package/src/config.ts +0 -1
- package/src/events.ts +0 -1
- package/src/gateway.ts +0 -1
- package/src/index.ts +0 -304
- package/src/mcp.ts +0 -1
- package/src/multimodal.ts +0 -1
- package/src/scheduler.ts +0 -1
- package/src/security.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/state.ts +0 -1
- package/src/storage.ts +0 -1
- package/src/tools.ts +0 -1
- package/src/tts.ts +0 -1
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -1
- package/src/voice.ts +0 -1
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hive CronScheduler
|
|
3
|
+
*
|
|
4
|
+
* Croner-based scheduler for Hive with SQLite persistence.
|
|
5
|
+
* Manages recurring and one-shot cron jobs that execute through the agent pipeline.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Cron } from "croner";
|
|
9
|
+
import type { Database } from "bun:sqlite";
|
|
10
|
+
import { logger } from "../utils/logger";
|
|
11
|
+
import { notifyTaskCompletion } from "./integration";
|
|
12
|
+
import type {
|
|
13
|
+
CronJob,
|
|
14
|
+
TaskRun,
|
|
15
|
+
CreateCronJobInput,
|
|
16
|
+
UpdateCronJobInput,
|
|
17
|
+
CronJobStatus,
|
|
18
|
+
CronJobExecutionHandler,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
const log = logger.child("CronScheduler");
|
|
22
|
+
|
|
23
|
+
export class CronScheduler {
|
|
24
|
+
private jobs: Map<string, Cron> = new Map();
|
|
25
|
+
private db: Database;
|
|
26
|
+
private handler: CronJobExecutionHandler;
|
|
27
|
+
private cleanupTaskId: string | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(db: Database, handler: CronJobExecutionHandler) {
|
|
30
|
+
this.db = db;
|
|
31
|
+
this.handler = handler;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Boot the scheduler - load all active jobs from DB and activate them
|
|
36
|
+
*/
|
|
37
|
+
boot(): void {
|
|
38
|
+
const tasks = this.db.query(`
|
|
39
|
+
SELECT * FROM cron_jobs WHERE status = 'active'
|
|
40
|
+
`).all() as CronJob[];
|
|
41
|
+
|
|
42
|
+
for (const task of tasks) {
|
|
43
|
+
this.activate(task);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log.info(`[boot] Loaded ${tasks.length} active job(s)`);
|
|
47
|
+
|
|
48
|
+
this.ensureCleanupTask();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Activate a cron job - create or recreate its Croner instance
|
|
53
|
+
*/
|
|
54
|
+
activate(task: CronJob): void {
|
|
55
|
+
const existingJob = this.jobs.get(task.id);
|
|
56
|
+
if (existingJob) {
|
|
57
|
+
existingJob.stop();
|
|
58
|
+
this.jobs.delete(task.id);
|
|
59
|
+
log.debug(`[activate] Stopped existing job for task "${task.name}" (${task.id})`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (task.status === "paused" || task.status === "completed" || task.status === "cancelled") {
|
|
63
|
+
log.debug(`[activate] Skipping job "${task.name}" (${task.id}) - status: ${task.status}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fix 2A: auto-pause jobs that exceeded the error threshold
|
|
68
|
+
const MAX_ERRORS = 5;
|
|
69
|
+
if (task.error_count >= MAX_ERRORS) {
|
|
70
|
+
this.db.query(
|
|
71
|
+
"UPDATE cron_jobs SET status = 'paused', last_error = ?, updated_at = ? WHERE id = ?"
|
|
72
|
+
).run(`Auto-paused after ${MAX_ERRORS} consecutive errors`, new Date().toISOString(), task.id);
|
|
73
|
+
log.warn(`[activate] Job "${task.name}" (${task.id}) auto-paused (error_count=${task.error_count})`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
let pattern: string;
|
|
79
|
+
if (task.task_type === "recurring") {
|
|
80
|
+
if (!task.cron_expression) {
|
|
81
|
+
log.error(`[activate] Job "${task.name}" (${task.id}) is recurring but has no cron_expression`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
pattern = task.cron_expression;
|
|
85
|
+
} else {
|
|
86
|
+
if (!task.fire_at) {
|
|
87
|
+
log.error(`[activate] Job "${task.name}" (${task.id}) is one_shot but has no fire_at`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
pattern = task.fire_at;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
new Cron(pattern);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
log.error(`[activate] Invalid cron pattern "${pattern}" for job "${task.name}": ${(err as Error).message}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const options: any = {
|
|
101
|
+
timezone: task.timezone,
|
|
102
|
+
protect: task.protect === 1,
|
|
103
|
+
catch: (error: Error) => this.handleError(task, error),
|
|
104
|
+
name: task.name,
|
|
105
|
+
domAndDow: task.dom_and_dow === 1,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (task.max_runs !== null && task.max_runs !== undefined) {
|
|
109
|
+
options.maxRuns = task.max_runs;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (task.interval_sec !== null && task.interval_sec !== undefined) {
|
|
113
|
+
options.interval = task.interval_sec;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (task.start_at) {
|
|
117
|
+
options.startAt = task.start_at;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (task.stop_at) {
|
|
121
|
+
options.stopAt = task.stop_at;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fix 1: pass only the ID so execute always reads fresh data from DB
|
|
125
|
+
const cron = new Cron(
|
|
126
|
+
pattern,
|
|
127
|
+
options,
|
|
128
|
+
() => this.execute(task.id)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
this.jobs.set(task.id, cron);
|
|
132
|
+
|
|
133
|
+
const nextRun = cron.nextRun();
|
|
134
|
+
if (nextRun) {
|
|
135
|
+
const nextRunIso = nextRun.toISOString();
|
|
136
|
+
// Fix 4: also update updated_at when writing next_run_at
|
|
137
|
+
this.db.query(
|
|
138
|
+
"UPDATE cron_jobs SET next_run_at = ?, updated_at = ? WHERE id = ?"
|
|
139
|
+
).run(nextRunIso, new Date().toISOString(), task.id);
|
|
140
|
+
log.info(`[activate] Job "${task.name}" (${task.id}) scheduled - next: ${nextRunIso}`);
|
|
141
|
+
} else {
|
|
142
|
+
log.warn(`[activate] Job "${task.name}" (${task.id}) has no next run date`);
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
log.error(`[activate] Failed to activate job "${task.name}" (${task.id}): ${(err as Error).message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Execute a cron job - run it through the agent pipeline
|
|
151
|
+
*/
|
|
152
|
+
private async execute(taskId: string): Promise<void> {
|
|
153
|
+
// Fix 1: read fresh task data from DB to avoid stale closure snapshots
|
|
154
|
+
const task = this.db.query("SELECT * FROM cron_jobs WHERE id = ?").get(taskId) as CronJob | null;
|
|
155
|
+
if (!task) {
|
|
156
|
+
log.warn(`[execute] Job "${taskId}" not found in DB — skipping`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const runId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
161
|
+
const startedAt = new Date().toISOString();
|
|
162
|
+
const startTime = performance.now();
|
|
163
|
+
|
|
164
|
+
log.info(`[execute] Starting job "${task.name}" (${task.id}) run #${runId}`);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
this.db.query(`
|
|
168
|
+
INSERT INTO task_runs (id, task_id, status, started_at, payload_snapshot)
|
|
169
|
+
VALUES (?, ?, 'running', ?, ?)
|
|
170
|
+
`).run(runId, task.id, startedAt, task.payload);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
log.error(`[execute] Failed to create task_run record: ${(err as Error).message}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const result = await this.handler(task);
|
|
177
|
+
const duration = performance.now() - startTime;
|
|
178
|
+
const finishedAt = new Date().toISOString();
|
|
179
|
+
|
|
180
|
+
if (result.success) {
|
|
181
|
+
this.db.query(`
|
|
182
|
+
UPDATE task_runs
|
|
183
|
+
SET status = 'success', finished_at = ?, duration_ms = ?, agent_response = ?
|
|
184
|
+
WHERE id = ?
|
|
185
|
+
`).run(finishedAt, Math.round(duration), result.response?.slice(0, 1000) || null, runId);
|
|
186
|
+
|
|
187
|
+
this.db.query(`
|
|
188
|
+
UPDATE cron_jobs
|
|
189
|
+
SET run_count = run_count + 1, last_run_at = ?, last_error = NULL
|
|
190
|
+
WHERE id = ?
|
|
191
|
+
`).run(finishedAt, task.id);
|
|
192
|
+
|
|
193
|
+
const job = this.jobs.get(task.id);
|
|
194
|
+
if (job) {
|
|
195
|
+
const nextRun = job.nextRun();
|
|
196
|
+
if (nextRun) {
|
|
197
|
+
this.db.query(
|
|
198
|
+
"UPDATE cron_jobs SET next_run_at = ? WHERE id = ?"
|
|
199
|
+
).run(nextRun.toISOString(), task.id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await notifyTaskCompletion(task.id, task.name, true, result.response);
|
|
204
|
+
|
|
205
|
+
if (task.task_type === "one_shot") {
|
|
206
|
+
this.db.query(`
|
|
207
|
+
UPDATE cron_jobs
|
|
208
|
+
SET status = 'completed', completed_at = ?
|
|
209
|
+
WHERE id = ?
|
|
210
|
+
`).run(finishedAt, task.id);
|
|
211
|
+
this.deactivate(task.id);
|
|
212
|
+
log.info(`[execute] One-shot job "${task.name}" (${task.id}) completed`);
|
|
213
|
+
} else {
|
|
214
|
+
log.info(`[execute] Job "${task.name}" (${task.id}) completed in ${Math.round(duration)}ms`);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
throw new Error(result.error || "Handler reported failure");
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
const duration = performance.now() - startTime;
|
|
221
|
+
const finishedAt = new Date().toISOString();
|
|
222
|
+
const errorMessage = (err as Error).message;
|
|
223
|
+
|
|
224
|
+
this.db.query(`
|
|
225
|
+
UPDATE task_runs
|
|
226
|
+
SET status = 'failed', finished_at = ?, duration_ms = ?, error_message = ?
|
|
227
|
+
WHERE id = ?
|
|
228
|
+
`).run(finishedAt, Math.round(duration), errorMessage, runId);
|
|
229
|
+
|
|
230
|
+
this.db.query(`
|
|
231
|
+
UPDATE cron_jobs
|
|
232
|
+
SET error_count = error_count + 1, last_error = ?
|
|
233
|
+
WHERE id = ?
|
|
234
|
+
`).run(errorMessage, task.id);
|
|
235
|
+
|
|
236
|
+
log.error(`[execute] Job "${task.name}" (${task.id}) failed: ${errorMessage}`);
|
|
237
|
+
|
|
238
|
+
// Fix 2B: auto-pause if error threshold reached
|
|
239
|
+
const MAX_ERRORS = 5;
|
|
240
|
+
const updated = this.db.query(
|
|
241
|
+
"SELECT error_count FROM cron_jobs WHERE id = ?"
|
|
242
|
+
).get(task.id) as { error_count: number } | null;
|
|
243
|
+
|
|
244
|
+
if (updated && updated.error_count >= MAX_ERRORS) {
|
|
245
|
+
this.db.query(
|
|
246
|
+
"UPDATE cron_jobs SET status = 'paused', updated_at = ? WHERE id = ?"
|
|
247
|
+
).run(new Date().toISOString(), task.id);
|
|
248
|
+
this.deactivate(task.id);
|
|
249
|
+
log.warn(`[execute] Job "${task.name}" (${task.id}) auto-paused after ${MAX_ERRORS} errors`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await notifyTaskCompletion(task.id, task.name, false, undefined, errorMessage);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Handle errors from Croner
|
|
258
|
+
*/
|
|
259
|
+
private handleError(task: CronJob, error: Error): void {
|
|
260
|
+
log.error(`[error] Job "${task.name}" (${task.id}) error: ${error.message}`);
|
|
261
|
+
|
|
262
|
+
// Fix 3: record Croner-level errors in task_runs for full history
|
|
263
|
+
const runId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
264
|
+
const now = new Date().toISOString();
|
|
265
|
+
try {
|
|
266
|
+
this.db.query(`
|
|
267
|
+
INSERT INTO task_runs (id, task_id, status, started_at, finished_at, duration_ms, error_message)
|
|
268
|
+
VALUES (?, ?, 'failed', ?, ?, 0, ?)
|
|
269
|
+
`).run(runId, task.id, now, now, error.message);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
log.warn(`[handleError] Failed to insert task_run: ${(e as Error).message}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.db.query(`
|
|
275
|
+
UPDATE cron_jobs
|
|
276
|
+
SET error_count = error_count + 1, last_error = ?
|
|
277
|
+
WHERE id = ?
|
|
278
|
+
`).run(error.message, task.id);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Pause a cron job
|
|
283
|
+
*/
|
|
284
|
+
pause(taskId: string): boolean {
|
|
285
|
+
const job = this.jobs.get(taskId);
|
|
286
|
+
if (job) {
|
|
287
|
+
job.pause();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const result = this.db.query(
|
|
291
|
+
"UPDATE cron_jobs SET status = 'paused' WHERE id = ?"
|
|
292
|
+
).run(taskId);
|
|
293
|
+
|
|
294
|
+
if (result.changes > 0) {
|
|
295
|
+
log.info(`[pause] Job "${taskId}" paused`);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
log.warn(`[pause] Job "${taskId}" not found`);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resume a paused cron job
|
|
305
|
+
*/
|
|
306
|
+
resume(taskId: string): boolean {
|
|
307
|
+
const task = this.db.query(
|
|
308
|
+
"SELECT * FROM cron_jobs WHERE id = ?"
|
|
309
|
+
).get(taskId) as CronJob | undefined;
|
|
310
|
+
|
|
311
|
+
if (!task) {
|
|
312
|
+
log.warn(`[resume] Job "${taskId}" not found`);
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.db.query(
|
|
317
|
+
"UPDATE cron_jobs SET status = 'active' WHERE id = ?"
|
|
318
|
+
).run(taskId);
|
|
319
|
+
|
|
320
|
+
this.activate(task);
|
|
321
|
+
log.info(`[resume] Job "${taskId}" resumed`);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Deactivate a cron job - stop Croner instance but keep in DB
|
|
327
|
+
*/
|
|
328
|
+
deactivate(taskId: string): void {
|
|
329
|
+
const job = this.jobs.get(taskId);
|
|
330
|
+
if (job) {
|
|
331
|
+
job.stop();
|
|
332
|
+
this.jobs.delete(taskId);
|
|
333
|
+
log.debug(`[deactivate] Job "${taskId}" deactivated`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Delete a cron job - deactivate and remove from DB
|
|
339
|
+
*/
|
|
340
|
+
delete(taskId: string): boolean {
|
|
341
|
+
this.deactivate(taskId);
|
|
342
|
+
|
|
343
|
+
const result = this.db.query(
|
|
344
|
+
"DELETE FROM cron_jobs WHERE id = ?"
|
|
345
|
+
).run(taskId);
|
|
346
|
+
|
|
347
|
+
if (result.changes > 0) {
|
|
348
|
+
log.info(`[delete] Job "${taskId}" deleted`);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
log.warn(`[delete] Job "${taskId}" not found`);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Create a new cron job
|
|
358
|
+
*/
|
|
359
|
+
create(input: CreateCronJobInput): { id: string; nextRun?: string } {
|
|
360
|
+
const id = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
361
|
+
const now = new Date().toISOString();
|
|
362
|
+
|
|
363
|
+
if (input.task_type === "recurring") {
|
|
364
|
+
if (!input.cron_expression) {
|
|
365
|
+
throw new Error("recurring task requires cron_expression");
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
new Cron(input.cron_expression);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
throw new Error(`Invalid cron expression: ${(err as Error).message}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (input.task_type === "one_shot") {
|
|
375
|
+
if (!input.fire_at) {
|
|
376
|
+
throw new Error("one_shot task requires fire_at");
|
|
377
|
+
}
|
|
378
|
+
const fireAt = new Date(input.fire_at);
|
|
379
|
+
if (fireAt.getTime() <= Date.now()) {
|
|
380
|
+
throw new Error("fire_at must be in the future");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
new Intl.DateTimeFormat(undefined, { timeZone: input.timezone });
|
|
386
|
+
} catch (err) {
|
|
387
|
+
throw new Error(`Invalid timezone: ${input.timezone}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const payloadJson = input.payload ? JSON.stringify(input.payload) : "{}";
|
|
391
|
+
try {
|
|
392
|
+
JSON.parse(payloadJson);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
throw new Error("Invalid payload JSON");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.db.query(`
|
|
398
|
+
INSERT INTO cron_jobs (
|
|
399
|
+
id, name, task, task_type, cron_expression, fire_at, timezone,
|
|
400
|
+
start_at, stop_at, dom_and_dow,
|
|
401
|
+
max_runs, protect, interval_sec, agent_id, channel, payload, tool_name,
|
|
402
|
+
status, created_at, updated_at
|
|
403
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
|
|
404
|
+
`).run(
|
|
405
|
+
id,
|
|
406
|
+
input.name,
|
|
407
|
+
input.task,
|
|
408
|
+
input.task_type,
|
|
409
|
+
input.cron_expression || null,
|
|
410
|
+
input.fire_at || null,
|
|
411
|
+
input.timezone,
|
|
412
|
+
input.start_at || null,
|
|
413
|
+
input.stop_at || null,
|
|
414
|
+
input.dom_and_dow ? 1 : 0,
|
|
415
|
+
input.max_runs || null,
|
|
416
|
+
input.protect !== false ? 1 : 0,
|
|
417
|
+
input.interval_sec || null,
|
|
418
|
+
input.agent_id || null,
|
|
419
|
+
input.channel || "system",
|
|
420
|
+
payloadJson,
|
|
421
|
+
input.tool_name || null,
|
|
422
|
+
now,
|
|
423
|
+
now
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const task = this.db.query(
|
|
427
|
+
"SELECT * FROM cron_jobs WHERE id = ?"
|
|
428
|
+
).get(id) as CronJob;
|
|
429
|
+
|
|
430
|
+
this.activate(task);
|
|
431
|
+
|
|
432
|
+
const job = this.jobs.get(id);
|
|
433
|
+
const nextRun = job?.nextRun()?.toISOString();
|
|
434
|
+
|
|
435
|
+
log.info(`[create] Job "${input.name}" (${id}) created`);
|
|
436
|
+
|
|
437
|
+
return { id, nextRun };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Update an existing cron job
|
|
442
|
+
*/
|
|
443
|
+
update(taskId: string, changes: UpdateCronJobInput): boolean {
|
|
444
|
+
const task = this.db.query(
|
|
445
|
+
"SELECT * FROM cron_jobs WHERE id = ?"
|
|
446
|
+
).get(taskId) as CronJob | undefined;
|
|
447
|
+
|
|
448
|
+
if (!task) {
|
|
449
|
+
log.warn(`[update] Job "${taskId}" not found`);
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const fields: string[] = [];
|
|
454
|
+
const values: any[] = [];
|
|
455
|
+
|
|
456
|
+
if (changes.name !== undefined) {
|
|
457
|
+
fields.push("name = ?");
|
|
458
|
+
values.push(changes.name);
|
|
459
|
+
}
|
|
460
|
+
if (changes.task !== undefined) {
|
|
461
|
+
fields.push("task = ?");
|
|
462
|
+
values.push(changes.task);
|
|
463
|
+
}
|
|
464
|
+
if (changes.task_type !== undefined) {
|
|
465
|
+
fields.push("task_type = ?");
|
|
466
|
+
values.push(changes.task_type);
|
|
467
|
+
}
|
|
468
|
+
if (changes.cron_expression !== undefined) {
|
|
469
|
+
fields.push("cron_expression = ?");
|
|
470
|
+
values.push(changes.cron_expression);
|
|
471
|
+
}
|
|
472
|
+
if (changes.fire_at !== undefined) {
|
|
473
|
+
fields.push("fire_at = ?");
|
|
474
|
+
values.push(changes.fire_at);
|
|
475
|
+
}
|
|
476
|
+
if (changes.timezone !== undefined) {
|
|
477
|
+
fields.push("timezone = ?");
|
|
478
|
+
values.push(changes.timezone);
|
|
479
|
+
}
|
|
480
|
+
if (changes.start_at !== undefined) {
|
|
481
|
+
fields.push("start_at = ?");
|
|
482
|
+
values.push(changes.start_at);
|
|
483
|
+
}
|
|
484
|
+
if (changes.stop_at !== undefined) {
|
|
485
|
+
fields.push("stop_at = ?");
|
|
486
|
+
values.push(changes.stop_at);
|
|
487
|
+
}
|
|
488
|
+
if (changes.dom_and_dow !== undefined) {
|
|
489
|
+
fields.push("dom_and_dow = ?");
|
|
490
|
+
values.push(changes.dom_and_dow ? 1 : 0);
|
|
491
|
+
}
|
|
492
|
+
if (changes.agent_id !== undefined) {
|
|
493
|
+
fields.push("agent_id = ?");
|
|
494
|
+
values.push(changes.agent_id);
|
|
495
|
+
}
|
|
496
|
+
if (changes.channel !== undefined) {
|
|
497
|
+
fields.push("channel = ?");
|
|
498
|
+
values.push(changes.channel);
|
|
499
|
+
}
|
|
500
|
+
if (changes.payload !== undefined) {
|
|
501
|
+
fields.push("payload = ?");
|
|
502
|
+
values.push(JSON.stringify(changes.payload));
|
|
503
|
+
}
|
|
504
|
+
if (changes.tool_name !== undefined) {
|
|
505
|
+
fields.push("tool_name = ?");
|
|
506
|
+
values.push(changes.tool_name);
|
|
507
|
+
}
|
|
508
|
+
if (changes.max_runs !== undefined) {
|
|
509
|
+
fields.push("max_runs = ?");
|
|
510
|
+
values.push(changes.max_runs);
|
|
511
|
+
}
|
|
512
|
+
if (changes.protect !== undefined) {
|
|
513
|
+
fields.push("protect = ?");
|
|
514
|
+
values.push(changes.protect ? 1 : 0);
|
|
515
|
+
}
|
|
516
|
+
if (changes.interval_sec !== undefined) {
|
|
517
|
+
fields.push("interval_sec = ?");
|
|
518
|
+
values.push(changes.interval_sec);
|
|
519
|
+
}
|
|
520
|
+
if (changes.status !== undefined) {
|
|
521
|
+
fields.push("status = ?");
|
|
522
|
+
values.push(changes.status);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (fields.length === 0) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
values.push(taskId);
|
|
530
|
+
this.db.query(`UPDATE cron_jobs SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
531
|
+
|
|
532
|
+
const updatedTask = this.db.query(
|
|
533
|
+
"SELECT * FROM cron_jobs WHERE id = ?"
|
|
534
|
+
).get(taskId) as CronJob;
|
|
535
|
+
|
|
536
|
+
this.activate(updatedTask);
|
|
537
|
+
|
|
538
|
+
log.info(`[update] Job "${taskId}" updated`);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get status of all cron jobs
|
|
544
|
+
*/
|
|
545
|
+
getStatus(): CronJobStatus[] {
|
|
546
|
+
const tasks = this.db.query(
|
|
547
|
+
"SELECT id, name, status FROM cron_jobs ORDER BY id"
|
|
548
|
+
).all() as Array<{ id: string; name: string; status: string }>;
|
|
549
|
+
|
|
550
|
+
return tasks.map((task) => {
|
|
551
|
+
const job = this.jobs.get(task.id);
|
|
552
|
+
return {
|
|
553
|
+
id: task.id,
|
|
554
|
+
name: task.name,
|
|
555
|
+
nextRun: job?.nextRun() || null,
|
|
556
|
+
isBusy: job?.isBusy() || false,
|
|
557
|
+
status: task.status as any,
|
|
558
|
+
};
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Manually trigger a cron job execution
|
|
564
|
+
*/
|
|
565
|
+
trigger(taskId: string): boolean {
|
|
566
|
+
const task = this.db.query(
|
|
567
|
+
"SELECT * FROM cron_jobs WHERE id = ?"
|
|
568
|
+
).get(taskId) as CronJob | undefined;
|
|
569
|
+
|
|
570
|
+
if (!task) {
|
|
571
|
+
log.warn(`[trigger] Job "${taskId}" not found`);
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const job = this.jobs.get(taskId);
|
|
576
|
+
if (!job) {
|
|
577
|
+
log.warn(`[trigger] Job "${taskId}" has no active job`);
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
job.trigger();
|
|
582
|
+
log.info(`[trigger] Job "${taskId}" manually triggered`);
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Shutdown the scheduler - stop all jobs
|
|
588
|
+
*/
|
|
589
|
+
shutdown(): void {
|
|
590
|
+
for (const [id, job] of this.jobs.entries()) {
|
|
591
|
+
job.stop();
|
|
592
|
+
}
|
|
593
|
+
this.jobs.clear();
|
|
594
|
+
log.info("[shutdown] All jobs stopped");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Ensure the cleanup job exists
|
|
599
|
+
*/
|
|
600
|
+
private ensureCleanupTask(): void {
|
|
601
|
+
const existing = this.db.query(
|
|
602
|
+
"SELECT id FROM cron_jobs WHERE name = '_hive_cleanup_runs'"
|
|
603
|
+
).get() as { id: string } | undefined;
|
|
604
|
+
|
|
605
|
+
if (existing) {
|
|
606
|
+
this.cleanupTaskId = existing.id;
|
|
607
|
+
log.debug("[ensureCleanupTask] Cleanup job already exists");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const result = this.create({
|
|
613
|
+
name: "_hive_cleanup_runs",
|
|
614
|
+
task: "Automatic cleanup of old task_runs and completed one_shot jobs",
|
|
615
|
+
task_type: "recurring",
|
|
616
|
+
cron_expression: "0 4 * * *",
|
|
617
|
+
timezone: "UTC",
|
|
618
|
+
payload: { _internal: true, action: "cleanup" },
|
|
619
|
+
protect: true,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
this.cleanupTaskId = result.id;
|
|
623
|
+
log.info("[ensureCleanupTask] Cleanup job created");
|
|
624
|
+
} catch (err) {
|
|
625
|
+
log.error(`[ensureCleanupTask] Failed to create cleanup job: ${(err as Error).message}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Run cleanup - called by the internal cleanup job
|
|
631
|
+
*/
|
|
632
|
+
runCleanup(): void {
|
|
633
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
634
|
+
this.db.query(`
|
|
635
|
+
DELETE FROM task_runs
|
|
636
|
+
WHERE status IN ('success', 'failed') AND started_at < ?
|
|
637
|
+
`).run(thirtyDaysAgo);
|
|
638
|
+
|
|
639
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
640
|
+
this.db.query(`
|
|
641
|
+
UPDATE cron_jobs
|
|
642
|
+
SET status = 'cancelled'
|
|
643
|
+
WHERE task_type = 'one_shot' AND status = 'completed' AND completed_at < ?
|
|
644
|
+
`).run(sevenDaysAgo);
|
|
645
|
+
|
|
646
|
+
const tasks = this.db.query(`
|
|
647
|
+
SELECT DISTINCT task_id FROM task_runs
|
|
648
|
+
`).all() as { task_id: string }[];
|
|
649
|
+
|
|
650
|
+
for (const { task_id } of tasks) {
|
|
651
|
+
this.db.query(`
|
|
652
|
+
DELETE FROM task_runs
|
|
653
|
+
WHERE task_id = ? AND id NOT IN (
|
|
654
|
+
SELECT id FROM task_runs
|
|
655
|
+
WHERE task_id = ?
|
|
656
|
+
ORDER BY started_at DESC
|
|
657
|
+
LIMIT 1000
|
|
658
|
+
)
|
|
659
|
+
`).run(task_id, task_id);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
log.info("[runCleanup] Cleanup completed");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Get task run history
|
|
667
|
+
*/
|
|
668
|
+
getHistory(taskId: string, limit = 50): TaskRun[] {
|
|
669
|
+
return this.db.query(`
|
|
670
|
+
SELECT * FROM task_runs
|
|
671
|
+
WHERE task_id = ?
|
|
672
|
+
ORDER BY started_at DESC
|
|
673
|
+
LIMIT ?
|
|
674
|
+
`).all(taskId, limit) as TaskRun[];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Get a single cron job by ID
|
|
679
|
+
*/
|
|
680
|
+
getTask(taskId: string): CronJob | null {
|
|
681
|
+
return this.db.query(
|
|
682
|
+
"SELECT * FROM cron_jobs WHERE id = ?"
|
|
683
|
+
).get(taskId) as CronJob | null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* List all cron jobs
|
|
688
|
+
*/
|
|
689
|
+
listTasks(status?: string): CronJob[] {
|
|
690
|
+
if (status) {
|
|
691
|
+
return this.db.query(
|
|
692
|
+
"SELECT * FROM cron_jobs WHERE status = ? ORDER BY next_run_at"
|
|
693
|
+
).all(status) as CronJob[];
|
|
694
|
+
}
|
|
695
|
+
return this.db.query(
|
|
696
|
+
"SELECT * FROM cron_jobs ORDER BY next_run_at"
|
|
697
|
+
).all() as CronJob[];
|
|
698
|
+
}
|
|
699
|
+
}
|