Files
cobble/src/functions/backend/db/cron-jobs/create-cron-job.ts
T
tbenandClaude Opus 5.5 c44048dbab Add bot schedules (routines), open Activity, add Help and a Desktop computer view
Bots create, list, update and delete schedules (cron_jobs, Bun.cron in UTC);
a per-minute cron queues due runs as tasks. Routines, Activity (super admins)
and Help get their own rail aside. Help covers familiar problems, starting
with signing in to Google through Cookie-Editor over the new Desktop (noVNC)
view.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 10:52:32 +01:00

109 lines
3.3 KiB
TypeScript

import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_CRON_JOBS, BunSQLiteTables } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import AppError from "@/src/utils/app-error";
import nextCronRun from "@/src/utils/next-cron-run";
import grabUserConversation from "../bot-sessions/grab-user-conversation";
import grabBotComputers from "../computers/grab-bot-computers";
import grabUserCronJob from "./grab-user-cron-job";
type Params = {
user_id: number | string;
bot_id: number | string;
/**
* The workspace it runs in; none for the bot's own chat.
*/
workspace_id?: number | string | null;
title: string;
instructions: string;
/**
* A Bun.cron expression, in UTC.
*/
schedule: string;
/**
* Must be one of the bot's computers there; none runs on its current
* one.
*/
computer_id?: number | string | null;
max_runs?: number | null;
};
/**
* Gives one of the user's bots a schedule: its brief is queued as a task
* at every run (`fireCronJob`), in the bot's conversation there. Returns
* the new schedule.
*/
export default async function createCronJob({
user_id,
bot_id,
workspace_id,
title,
instructions,
schedule,
computer_id,
max_runs,
}: Params) {
const conversation = await grabUserConversation({ user_id, bot_id, workspace_id });
const bot_id_num = conversation.bot.id;
const count = Number(
(
await BunSQLite.sql<{ n: number }>({
sql: `SELECT COUNT(*) AS n FROM cron_jobs WHERE bot_id = ?`,
values: [bot_id_num],
})
).payload?.[0]?.n || 0,
);
if (count >= AppData.CronJobMaxPerBot) {
throw new AppError(
`${conversation.bot.name || "This bot"} already has ${AppData.CronJobMaxPerBot} schedules. Delete one first.`,
);
}
if (computer_id) {
const computers = await grabBotComputers({
bot_id: bot_id_num,
user_id,
workspace_id: conversation.workspace_id,
});
if (!computers.some((c) => String(c.id) == String(computer_id))) {
throw new AppError(`This bot has no access to that computer.`);
}
}
const next_run_at = nextCronRun({ schedule, check_interval: true });
if (!next_run_at) {
throw new AppError(`"${schedule}" never runs.`);
}
const res = await BunSQLite.insert<BUN_SQLITE_COBBLE_CRON_JOBS, (typeof BunSQLiteTables)[number]>({
table: "cron_jobs",
data: [
{
user_id: Number(user_id),
bot_id: bot_id_num,
workspace_id: conversation.workspace_id || undefined,
title,
instructions,
schedule: schedule.trim(),
computer_id: computer_id ? Number(computer_id) : undefined,
enabled: 1,
max_runs: max_runs || undefined,
run_count: 0,
next_run_at,
},
],
});
const cron_job_id = res.singleRes?.id || res.postInsertReturn?.insertId;
if (!res.success || !cron_job_id) {
throw new Error(`Couldn't save the schedule: ${res.msg}`);
}
return await grabUserCronJob({ user_id, cron_job_id });
}