gadget/gadget-tasks/src/services/scheduler.ts
Rob Colbert b906cb2377 feat(gadget-tasks): course correction — headless IDE client architecture
Converts gadget-tasks from a duplicated codebase with direct MongoDB access
to a headless IDE client that uses gadget-code's REST API and Socket.IO
protocol to submit and process task prompts through the existing pipeline.

Deleted (no longer needed):
- gadget-tasks/src/models/ (5 Mongoose models — duplicated from gadget-code)
- gadget-tasks/data/prompts/ (11 prompt templates — duplicated from gadget-code)
- gadget-tasks/src/services/executor.ts (629-line AWL loop — duplicated from gadget-drone)
- gadget-tasks/src/services/ai.ts (direct AI API calls — drones do this now)
- gadget-tasks/src/services/workspace.ts (workspace management — drones do this now)

Added:
- gadget-tasks/src/services/platform.ts — REST API + Socket.IO headless IDE client
  with session lock, workspace mode, prompt submission, work order tracking

Rewritten:
- gadget-tasks/src/gadget-tasks.ts — new startup: user login → auth → drone
  selection → Socket.IO connect → schedule tasks (no MongoDB)
- gadget-tasks/src/services/scheduler.ts — delegates to PlatformService.executeTask()
- gadget-tasks/src/config/env.ts — platform.baseUrl config, no mongodb/google

gadget-code changes:
- Added PATCH /api/v1/projects/:projectId/tasks/:taskId/lastRun route
- Added ProjectService.updateTaskLastRun() for atomic task field update

Config changes:
- GadgetTasksConfig: replaced mongodb with platform.baseUrl, removed google.cse
- Dependencies: removed mongoose, @gadget/ai, @gadget/ai-toolbox, dayjs,
  simple-git, nanoid; added socket.io-client, @inquirer/prompts
2026-05-17 00:48:39 -04:00

134 lines
3.2 KiB
TypeScript

// src/services/scheduler.ts
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0
import env from "../config/env.ts";
import { CronJob } from "cron";
import type { IProjectTask, IProject } from "@gadget/api";
import { GadgetService } from "../lib/service.ts";
import PlatformService from "./platform.ts";
class SchedulerService extends GadgetService {
private jobs: Map<string, CronJob> = new Map();
private _activeCount: number = 0;
get name(): string {
return "SchedulerService";
}
get slug(): string {
return "svc:scheduler";
}
get activeCount(): number {
return this._activeCount;
}
get scheduledCount(): number {
return this.jobs.size;
}
async start(): Promise<void> {
this.log.info("started");
}
async stop(): Promise<void> {
for (const [taskId, job] of this.jobs) {
job.stop();
this.log.info("stopped cron job", { taskId });
}
this.jobs.clear();
this.log.info("stopped");
}
/**
* Schedules a task by creating a CronJob.
* Key: task._id (string)
*/
scheduleTask(task: IProjectTask, project: IProject): void {
// If already scheduled, unschedule first
const taskId = String(task._id);
if (this.jobs.has(taskId)) {
this.unscheduleTask(taskId);
}
try {
const job = new CronJob(
task.crontab, // cron expression
async () => {
await this.executeTask(task, project);
},
null, // onComplete
true, // start
env.timezone, // timeZone
);
this.jobs.set(taskId, job);
this.log.info("task scheduled", {
taskId,
taskName: task.name,
crontab: task.crontab,
projectSlug: project.slug,
});
} catch (error) {
const err = error as Error;
this.log.error("failed to schedule task", {
taskId,
crontab: task.crontab,
error: err.message,
});
}
}
/**
* Removes a scheduled task.
*/
unscheduleTask(taskId: string): void {
const job = this.jobs.get(taskId);
if (job) {
job.stop();
this.jobs.delete(taskId);
this.log.info("task unscheduled", { taskId });
}
}
/**
* Execute a task with concurrency control.
* Delegates to PlatformService.executeTask() which handles
* the full headless IDE pipeline.
*/
private async executeTask(task: IProjectTask, project: IProject): Promise<void> {
// Check concurrency
if (this._activeCount >= env.concurrency) {
this.log.warn("task skipped: concurrency limit reached", {
taskId: task._id,
taskName: task.name,
activeCount: this._activeCount,
maxConcurrency: env.concurrency,
});
return;
}
this._activeCount++;
try {
this.log.info("executing task", {
taskId: task._id,
taskName: task.name,
projectSlug: project.slug,
activeCount: this._activeCount,
});
await PlatformService.executeTask(task, project);
} catch (error) {
const err = error as Error;
this.log.error("task execution failed", {
taskId: task._id,
error: err.message,
});
} finally {
this._activeCount--;
}
}
}
export default new SchedulerService();