* initialize * expand tool result text limit | add syntax highlighting and json formatting to tool result rendering * fix onError jsdoc * lint * fix unread icon * route protection * improve form handling for NewJobModal * safeJsonParse * remove unneeded comments * remove trycatch * add truncateText helper * add explicit fallback value tos safeJsonParse * add shared cron constant and helpers * reduce frontend indirection * use isLight to compute syntax highlighting theme * remove dead code * remove forJob and make job limit to 50 * create recomputeNextRunAt helper method * add comment about nextRunAt recomputation * add job queue and concurrency control to scheduled jobs * use p-queue * change default max concurrent value to 1 * add comment explaining internal scheduling system * add recomputeNextRunAt on boot * add generated documents to run details * Modify toolsOverride functionality where no tools selected means no tools are given to the agent add a select all/deselect all toggle button for easily selecting all tools in the cerate job form * create usePolling hook * add polling to scheduled jobs and scheduled job runs pages * add cron generation feature in job form * remove cron generation feature | add cron builder feature | add max active scheduled jobs limit * set MAX_ACTIVE to null * replace hour and minute input fields with input with type time * simplify * organize components * move components to bottom of page component * change Generated Documents to Generated Files * add i18n to cronstrue * add i18n * add type="button" to button elements * refactor fileSource retrieval logic * one scheduled job run can have status "running" * add protection of file retrieveal from scheduled job in multiuser mode * fix comments * make job status default to queued * add queued status * fix bug with result trace rendering * store timeout ref and clearTimeout once race settles * remove unneeded handlerPromise tracking * move imports to top level * refactor hardcoded paths to path resolve functions * implement new job form design * simplify * fix button styles * fix runJob bug * implement styles for scheduled jobs page * apply dark mode figma styles * delete unused translation key * implement light mode for new new job modal, run history, and run details * lint * fix light mode scroll bar in tool call card * adjust table header contrast * fix type in subtitle * kill workers when job is in-flight before deleting job * add border-none to buttons * change locale time to iso string * import BackgroundService module level | instatiate backgroundService singltone once and reuse across handlers * add p-queue, @breejs/later and cron-validate as core deps * parse cron expression to a builder state once * add theme to day buttons in cron builder * fix stale tools selection caption * flip popover when popover clips screen height * make ScheduleJob.trigger() await the run insertion | disable run now button if job is in flight * regen table * refactor generated file card * refactor frontend * remove logs * major refactor for tool picking, fix bree/later bug * combine action endpoints, move contine to method * fix unoptimized query with include + take + order * fix dangerous use, refactor job to utils * add copy content to text response * improve notification system subscription for browser * remove unused translations * prevent gen-file cleanup job from deleting active job file generated references * rich text copy * Scheduled Jobs: Translations (#5482) * add locales for scheduled jobs * i18n --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com> * add config flag with UI notice * update README * telemetry datapoints * Always use UTC on backend, convert to local in frontend * fix tz render * Add job killing * cleanup thinking text in job notifications and break out reasoning in response text. Also hide zero metrics since that is useless * Port generatedFile schema to the normalized workspace chat `outputs` file format so porting to thread is simple and implem between chats <> jobs is 1:1 * what the fuck * compiled bug * fixed thinking oddity in complied frontend * supress multi-toast * fix duration call * Revert "fix duration call" This reverts commit 0491bc71f4223e65ea4046561b15b268fefb8da2. * revert and reapply fix --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
158 lines
5.0 KiB
JavaScript
158 lines
5.0 KiB
JavaScript
const { log, conclude } = require("./helpers/index.js");
|
|
const { v4: uuidv4 } = require("uuid");
|
|
const { safeJsonParse } = require("../utils/http");
|
|
const {
|
|
agentActionCb,
|
|
SCHEDULED_JOB_TIMEOUT_MS,
|
|
sendWebPushNotification,
|
|
} = require("./helpers/scheduled-job-helper.js");
|
|
const { ScheduledJob } = require("../models/scheduledJob.js");
|
|
const { ScheduledJobRun } = require("../models/scheduledJobRun.js");
|
|
|
|
/** Status of the scheduled job run @type {'success' | 'failed' | 'timed_out' | 'not_found' | 'killed' | undefined} */
|
|
let status;
|
|
let runId = null;
|
|
|
|
process.on("SIGTERM", async () => {
|
|
status = "killed";
|
|
log("Received SIGTERM, marking job as killed by user");
|
|
if (runId) await ScheduledJobRun.kill(runId);
|
|
conclude();
|
|
});
|
|
|
|
process.on("message", async (payload) => {
|
|
const { jobId, runId: payloadRunId } = payload;
|
|
runId = payloadRunId;
|
|
let timeoutId = null;
|
|
let errorMessage = null;
|
|
|
|
// The run row was created by the parent process (BackgroundService) in
|
|
// status `queued` (it may have been waiting in p-queue). The worker
|
|
// transitions it to `running` here so `startedAt` reflects actual execution
|
|
// start, then runs to a terminal state. If the job has been deleted between
|
|
// enqueue and now, fail the row.
|
|
try {
|
|
if (!jobId || !runId) return;
|
|
|
|
const job = await ScheduledJob.get({ id: Number(jobId) });
|
|
if (!job) {
|
|
log(`Scheduled job ${jobId} not found`);
|
|
status = "not_found";
|
|
return;
|
|
}
|
|
|
|
// Transition queued -> running. If this returns false, the row was
|
|
// already moved to a terminal state (e.g. parent failed it because it
|
|
// thought the worker had died). Bail out without touching it further.
|
|
const transitioned = await ScheduledJobRun.markRunning(runId);
|
|
if (!transitioned) {
|
|
log(
|
|
`Scheduled job "${job.name}" (id=${job.id}) is no longer queued, skipping`
|
|
);
|
|
return;
|
|
}
|
|
|
|
log(
|
|
`Starting scheduled job: "${job.name}" (id=${job.id}) with timeout ${SCHEDULED_JOB_TIMEOUT_MS}ms`
|
|
);
|
|
await ScheduledJob.updateRunTimestamps(job.id);
|
|
const { handler, thoughts, toolCalls, state } = agentActionCb();
|
|
|
|
const { EphemeralAgentHandler } = require("../utils/agents/ephemeral.js");
|
|
const agentHandler = await new EphemeralAgentHandler({
|
|
uuid: uuidv4(),
|
|
prompt: job.prompt,
|
|
}).init();
|
|
|
|
// Tool overrides control which tools the agent can use:
|
|
// - Array with items: only those specific tools are loaded
|
|
// - Empty array: no tools are loaded
|
|
const toolOverrides = safeJsonParse(job.tools, []);
|
|
await agentHandler.createAIbitat({
|
|
handler,
|
|
toolOverrides,
|
|
});
|
|
|
|
// Auto-approve all tool invocations when running a scheduled job
|
|
agentHandler.aibitat.requestToolApproval = async () => {
|
|
log("Tool approval requested for scheduled job, auto-approving");
|
|
return {
|
|
approved: true,
|
|
message: "Auto-approved by scheduled job runner.",
|
|
};
|
|
};
|
|
|
|
// Capture tool results for the execution trace
|
|
agentHandler.aibitat.onToolCallResult(
|
|
({ toolName, arguments: args, result }) => {
|
|
toolCalls.push({
|
|
toolName,
|
|
arguments: args,
|
|
result,
|
|
timestamp: Date.now(),
|
|
});
|
|
}
|
|
);
|
|
|
|
const startTime = Date.now();
|
|
await Promise.race([
|
|
agentHandler.startAgentCluster(),
|
|
new Promise((_, reject) => {
|
|
timeoutId = setTimeout(
|
|
() => reject(new Error("SCHEDULED_JOB_TIMEOUT")),
|
|
SCHEDULED_JOB_TIMEOUT_MS
|
|
);
|
|
}),
|
|
]).finally(() => {
|
|
if (!timeoutId) return;
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
});
|
|
const duration = Date.now() - startTime;
|
|
|
|
// Get outputs from aibitat which include proper type info (e.g., PptxFileDownload, ExcelFileDownload)
|
|
// for correct re-rendering when porting to workspace chat
|
|
const outputs = agentHandler.getPendingOutputs();
|
|
|
|
status = "success";
|
|
await ScheduledJobRun.complete(runId, {
|
|
result: {
|
|
text: state.textResponse,
|
|
thoughts,
|
|
toolCalls,
|
|
outputs,
|
|
metrics: state.metrics,
|
|
duration,
|
|
},
|
|
});
|
|
log(`Scheduled job "${job.name}" completed in ${duration}ms)`);
|
|
await sendWebPushNotification(job, runId, state.textResponse, log);
|
|
} catch (error) {
|
|
if (error.message === "SCHEDULED_JOB_TIMEOUT") {
|
|
status = "timed_out";
|
|
log("Scheduled job timed out");
|
|
} else {
|
|
status = "failed";
|
|
log(`Scheduled job error: ${error.message}`);
|
|
errorMessage = error.message;
|
|
}
|
|
} finally {
|
|
switch (status) {
|
|
case "not_found":
|
|
await ScheduledJobRun.failIfNotTerminal(runId, "Job no longer exists");
|
|
break;
|
|
case "timed_out":
|
|
await ScheduledJobRun.timeout(runId);
|
|
break;
|
|
case "failed":
|
|
await ScheduledJobRun.fail(runId, { error: errorMessage });
|
|
break;
|
|
default: // Do nothing by default (success, killed, other)
|
|
break;
|
|
}
|
|
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
conclude();
|
|
}
|
|
});
|