feat(scheduler): add cron-style scheduled jobs (backend)

Adds a saved/named scheduling layer on top of Codeman's existing session
primitives. Distinct from the legacy run-now ScheduledRun concept.

- types/scheduler.ts: ScheduledJob + ScheduledJobRun
- state-store: persist scheduledJobs/scheduledJobRuns in ~/.codeman/state.json
- scheduler/scheduler-time.ts: pure once/interval/daily/weekly next-run math
- scheduler/scheduler-service.ts: CRUD, Run Now, due-checker tick, run history;
  reuses SessionPort (create -> start -> writeViaMux) for launches
- web/routes/scheduler-routes.ts: /api/scheduler/jobs CRUD + run + history
- web/schemas.ts: zod validation with schedule-type-aware refinements
- web/sse-events.ts: scheduler:* events
- server.ts: wire service into route context + 30s background tick loop
- test/scheduler-time.test.ts: 14 unit tests for next-run calculations

Phase 1 discovery recorded in SCHEDULER_DISCOVERY.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rp7JhmQXcYJhmxFMdZuuah
This commit is contained in:
Kris
2026-06-27 08:10:40 +05:30
co-authored by Claude Opus 4.8
parent abb6447f66
commit 6ae86b53f6
16 changed files with 1087 additions and 0 deletions
+23
View File
@@ -152,8 +152,10 @@ import {
registerClipboardRoutes,
registerSearchRoutes,
registerOrchestratorRoutes,
registerSchedulerRoutes,
registerWsRoutes,
} from './routes/index.js';
import { SchedulerService } from '../scheduler/scheduler-service.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -175,6 +177,7 @@ import {
ITERATION_PAUSE_MS,
STATS_COLLECTION_INTERVAL_MS,
INACTIVITY_TIMEOUT_MS,
SCHEDULER_TICK_INTERVAL,
} from '../config/server-timing.js';
/**
@@ -223,6 +226,8 @@ export class WebServer extends EventEmitter {
// Store session listener references for explicit cleanup (prevents memory leaks)
private sessionListenerRefs: Map<string, SessionListenerRefs> = new Map();
private scheduledRuns: Map<string, ScheduledRun> = new Map();
/** Cron-style scheduler service (assigned in setupRoutes). */
private schedulerService!: SchedulerService;
private sse: SseStreamManager;
private store = getStore();
private port: number;
@@ -873,6 +878,13 @@ export class WebServer extends EventEmitter {
registerClipboardRoutes(this.app, ctx);
registerSearchRoutes(this.app, ctx);
registerOrchestratorRoutes(this.app, ctx);
// Cron-style scheduler: build the service from the same context, recompute
// due times for any persisted jobs, then expose it to its routes.
this.schedulerService = new SchedulerService(ctx);
this.schedulerService.init();
registerSchedulerRoutes(this.app, { ...ctx, scheduler: this.schedulerService });
registerWsRoutes(this.app, ctx, () => this.getHostPolicy());
}
@@ -1947,6 +1959,17 @@ export class WebServer extends EventEmitter {
{ description: 'scheduled runs cleanup' }
);
// Start the cron-style scheduler loop (fires due ScheduledJobs).
this.cleanup.setInterval(
() => {
this.schedulerService.tickDueJobs().catch((err) => {
console.error('[scheduler] tick failed:', getErrorMessage(err));
});
},
SCHEDULER_TICK_INTERVAL,
{ description: 'scheduled jobs due-checker' }
);
// Start SSE client health check timer (prevents memory leaks from dead connections)
this.cleanup.setInterval(
() => {