jobs: run script tasks as background jobs via executeScript (phase 1b)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,11 @@ import {
|
||||
} from 'officerdb';
|
||||
import { executePipeline } from './pipeline-executor';
|
||||
import type { OutMessage } from './pipeline-executor';
|
||||
import { executeScript, type ScriptEvent } from './execute-script';
|
||||
|
||||
// Everything a job can stream — pipeline structural events plus script stdout/stderr/exit.
|
||||
type JobEvent = OutMessage | ScriptEvent;
|
||||
type JobMode = 'pipeline' | 'script' | 'agentic';
|
||||
|
||||
type WSData = {
|
||||
userId: number;
|
||||
@@ -24,7 +29,7 @@ type LiveJob = {
|
||||
userId: number;
|
||||
abortSignal: { aborted: boolean };
|
||||
emitter: EventEmitter;
|
||||
eventBuffer: OutMessage[];
|
||||
eventBuffer: JobEvent[];
|
||||
viewers: Set<ServerWebSocket<WSData>>;
|
||||
progressDirty: boolean;
|
||||
lastProgress: unknown;
|
||||
@@ -36,15 +41,16 @@ const PROGRESS_FLUSH_MS = 3000;
|
||||
|
||||
const liveJobs = new Map<string, LiveJob>();
|
||||
|
||||
function sendToViewer(ws: ServerWebSocket<WSData>, jobId: string, event: OutMessage) {
|
||||
function sendToViewer(ws: ServerWebSocket<WSData>, jobId: string, event: JobEvent) {
|
||||
if (ws.readyState === 1) {
|
||||
ws.send(JSON.stringify({ jobId, ...event }));
|
||||
}
|
||||
}
|
||||
|
||||
function broadcast(job: LiveJob, event: OutMessage) {
|
||||
// Buffer for replay — skip high-frequency deltas (only useful for live streaming)
|
||||
if (event.type !== 'assistant:delta') {
|
||||
function broadcast(job: LiveJob, event: JobEvent) {
|
||||
// Buffer for replay — skip high-frequency streams (deltas / script stdout+stderr). Those are
|
||||
// live-only; a late viewer backfills script output from the persisted log file instead.
|
||||
if (event.type !== 'assistant:delta' && event.type !== 'stdout' && event.type !== 'stderr') {
|
||||
job.eventBuffer.push(event);
|
||||
if (job.eventBuffer.length > EVENT_BUFFER_SIZE) {
|
||||
job.eventBuffer.shift();
|
||||
@@ -62,6 +68,8 @@ type StartJobParams = {
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
mode?: JobMode; // defaults to 'pipeline' for back-compat with the existing pipeline caller
|
||||
sandboxed?: boolean; // script jobs only
|
||||
taskDirName: string;
|
||||
taskName: string;
|
||||
inputs: Record<string, string>;
|
||||
@@ -73,6 +81,7 @@ type StartJobParams = {
|
||||
|
||||
export async function startJob(params: StartJobParams): Promise<string> {
|
||||
const jobId = randomUUID();
|
||||
const mode: JobMode = params.mode ?? 'pipeline';
|
||||
|
||||
// Create DB record
|
||||
await createPipelineJob({
|
||||
@@ -80,6 +89,7 @@ export async function startJob(params: StartJobParams): Promise<string> {
|
||||
userId: params.userId,
|
||||
taskDirName: params.taskDirName,
|
||||
taskName: params.taskName,
|
||||
mode,
|
||||
status: 'running',
|
||||
inputs: params.inputs,
|
||||
cwd: params.cwd ?? null,
|
||||
@@ -102,7 +112,7 @@ export async function startJob(params: StartJobParams): Promise<string> {
|
||||
liveJobs.set(jobId, job);
|
||||
|
||||
// Emit callback — broadcasts to viewers and tracks progress
|
||||
const emit = (event: OutMessage) => {
|
||||
const emit = (event: JobEvent) => {
|
||||
broadcast(job, event);
|
||||
|
||||
// Track progress for DB persistence (skip streaming messages — only structural events)
|
||||
@@ -139,25 +149,45 @@ export async function startJob(params: StartJobParams): Promise<string> {
|
||||
}
|
||||
}, PROGRESS_FLUSH_MS);
|
||||
|
||||
// Run the pipeline in the background
|
||||
executePipeline({
|
||||
userId: params.userId,
|
||||
email: params.email,
|
||||
username: params.username,
|
||||
role: params.role,
|
||||
taskDirName: params.taskDirName,
|
||||
inputs: params.inputs,
|
||||
cwd: params.cwd,
|
||||
model: params.model,
|
||||
startAt: params.startAt,
|
||||
abortSignal: job.abortSignal,
|
||||
emit,
|
||||
}).then(async () => {
|
||||
// Run the job in the background — dispatch by mode.
|
||||
const runner: Promise<void | { exitCode: number }> =
|
||||
mode === 'script'
|
||||
? executeScript({
|
||||
jobId,
|
||||
email: params.email,
|
||||
role: params.role,
|
||||
sandboxed: params.sandboxed ?? false,
|
||||
taskDirName: params.taskDirName,
|
||||
inputs: params.inputs,
|
||||
cwd: params.cwd,
|
||||
abortSignal: job.abortSignal,
|
||||
emit,
|
||||
})
|
||||
: executePipeline({
|
||||
userId: params.userId,
|
||||
email: params.email,
|
||||
username: params.username,
|
||||
role: params.role,
|
||||
taskDirName: params.taskDirName,
|
||||
inputs: params.inputs,
|
||||
cwd: params.cwd,
|
||||
model: params.model,
|
||||
startAt: params.startAt,
|
||||
abortSignal: job.abortSignal,
|
||||
emit,
|
||||
});
|
||||
|
||||
runner.then(async (result) => {
|
||||
clearInterval(flushInterval);
|
||||
// Script jobs resolve with an exit code — a non-zero exit is a failure. Pipelines resolve void.
|
||||
const exitCode = result && typeof result === 'object' && 'exitCode' in result ? result.exitCode : null;
|
||||
const failed = exitCode !== null && exitCode !== 0;
|
||||
await updatePipelineJob(jobId, {
|
||||
status: 'completed',
|
||||
status: failed ? 'failed' : 'completed',
|
||||
exitCode,
|
||||
progress: job.lastProgress as Record<string, unknown>,
|
||||
totalCost: job.lastCost as Record<string, unknown>,
|
||||
error: failed ? `Script exited with code ${exitCode}` : undefined,
|
||||
completedAt: new Date(),
|
||||
}).catch((err) => console.error(`[pipeline-jobs] failed to finalize ${jobId}:`, err));
|
||||
liveJobs.delete(jobId);
|
||||
@@ -243,8 +273,8 @@ export async function cleanupOnStartup() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract minimal progress info from events for DB storage. */
|
||||
function extractProgress(event: OutMessage, prev: unknown): unknown {
|
||||
/** Extract minimal progress info from events for DB storage. Script events fall through to `prev`. */
|
||||
function extractProgress(event: JobEvent, prev: unknown): unknown {
|
||||
const p = (prev as Record<string, unknown>) ?? {};
|
||||
|
||||
switch (event.type) {
|
||||
|
||||
Reference in New Issue
Block a user