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
+5
View File
@@ -23,6 +23,7 @@ import type { SessionState } from './session.js';
import type { TaskState } from './task.js';
import type { RalphLoopState } from './ralph.js';
import type { RespawnConfig } from './respawn.js';
import type { ScheduledJob, ScheduledJobRun } from './scheduler.js';
// ========== Global Stats Types ==========
@@ -111,6 +112,10 @@ export interface AppState {
tokenStats?: TokenStats;
/** Orchestrator Loop state (phased plan execution) */
orchestrator?: import('./orchestrator.js').OrchestratorPersistState;
/** Cron-style scheduled jobs, keyed by job ID. */
scheduledJobs?: Record<string, ScheduledJob>;
/** Scheduled job run history, keyed by run ID. */
scheduledJobRuns?: Record<string, ScheduledJobRun>;
}
// ========== Default Configuration ==========
+94
View File
@@ -0,0 +1,94 @@
/**
* @fileoverview Scheduled Jobs (cron-style scheduler) type definitions.
*
* NOTE: This is intentionally distinct from the existing `ScheduledRun` concept
* (see src/web/ports/infra-port.ts), which is a run-now, duration-bounded
* autonomous loop. A `ScheduledJob` is a SAVED, NAMED job with a recurring
* schedule (once/interval/daily/weekly), enable/disable, next-run calculation,
* and a history of `ScheduledJobRun` records. The two do not interact.
*
* Persisted to `~/.codeman/state.json` via StateStore (see AppState).
*/
import type { SessionMode } from './session.js';
/** How a job's fire times are computed. */
export type ScheduleType = 'once' | 'interval' | 'daily' | 'weekly';
/** Where the prompt text comes from. */
export type PromptMode = 'inline_text' | 'prompt_file_path';
/** How the prompt is delivered into the session. */
export type InputMode = 'paste' | 'typed';
/** Lifecycle status of a single job execution. */
export type ScheduledJobRunStatus = 'created' | 'session_started' | 'prompt_sent' | 'failed';
/** What triggered a run. */
export type TriggerType = 'scheduled' | 'manual_run_now';
/** What to do for an AUTOMATIC run when sessions of the same agent already exist. */
export type ConcurrencyPolicy = 'warn_only' | 'skip_if_same_agent_running';
/**
* A saved, named scheduled job.
*/
export interface ScheduledJob {
id: string;
name: string;
/** Reuses Codeman's existing session modes; 'shell' covers Terminal/custom. */
agentType: SessionMode;
workingDir: string;
/** Optional custom launch command (only meaningful for 'shell' mode). */
launchCommand?: string;
promptMode: PromptMode;
promptText?: string;
promptFilePath?: string;
inputMode: InputMode;
scheduleType: ScheduleType;
/** once: absolute epoch-ms fire time. */
runAt?: number;
/** interval: minutes between fires. */
intervalMinutes?: number;
/** daily: 'HH:MM' (24h, server-local time). */
dailyTime?: string;
/** weekly: weekdays 0–6 (0=Sunday). */
weeklyDays?: number[];
/** weekly: 'HH:MM' (24h, server-local time). */
weeklyTime?: string;
enabled: boolean;
notes?: string;
/** Applies to automatic (scheduled) runs only. Manual Run Now always warns client-side. */
concurrencyPolicy: ConcurrencyPolicy;
// ── Bookkeeping (server-maintained) ─────────────────────────────────────
createdAt: number;
updatedAt: number;
lastRunAt: number | null;
nextRunAt: number | null;
lastStatus: ScheduledJobRunStatus | null;
/** Duplicate-launch guard: identifies the most recent due-time consumed. */
lastDueKey: string | null;
/** True once a 'once' job has fired (it is also disabled). */
completedOnce?: boolean;
}
/**
* A single execution of a scheduled job (history record).
*/
export interface ScheduledJobRun {
id: string;
scheduledJobId: string;
sessionId: string | null;
sessionName: string | null;
startedAt: number;
finishedAt: number | null;
status: ScheduledJobRunStatus;
errorMessage?: string;
triggerType: TriggerType;
/** Best-effort deep link to the created session in the web UI. */
createdSessionUrl: string | null;
}