Skip to content

Collapse scheduled-task interval types to On-Demand or Scheduled, with Perpetual as an orthogonal drain flag #5829

Description

@atomantic

Problem / Goal

The CoS Schedule "Interval Type" dropdown currently offers seven mutually exclusive options (Rotation, Daily, Weekly, Once, On Demand, Cron, Perpetual). Several options duplicate cadence functionality, and the system cannot represent a task that is both perpetual (drains backlog until work-detector idles) and scheduled (starts its drain cycle via cron).

The goal is to simplify task cadence to a two-variant model (On-Demand vs Scheduled) while making perpetual an orthogonal boolean flag on any task schedule.

Target model:

  • Cadence model: type is either on-demand or cron (Scheduled in the UI).
  • Daily / Weekly options removed: Replaced by standard 5-field cron expressions via CronInput presets (0 7 * * *, 0 7 * * 1, etc.).
  • Once option removed: Represented as on-demand (manual trigger only; no auto-run or reset loop).
  • Perpetual flag: perpetual: boolean becomes an independent toggle available for both on-demand and cron tasks.
  • Rotation option deprecated: Removed from UI and runtime.

Existing persisted configurations must upgrade seamlessly in-place so no existing task silently changes cadence or starts unexpected execution.

Context

Current UI dropdown screenshot reference: data/screenshots/Screenshot_2026-09-01_at_11.37.49_PM.png.

Current Enums & Runtime Dispatch

  • Canonical Enum (8 values, 7 in global UI): server/services/taskScheduleConstants.js:6-15 (ROTATION, DAILY, WEEKLY, ONCE, ON_DEMAND, CUSTOM, CRON, PERPETUAL).
  • Runtime Dispatch: shouldRunTask in server/services/taskSchedule.js:888-1005 handles each type in a switch statement.
  • Queue Priority: getNextTaskType in server/services/taskSchedule.js:1068-1138 checks cron/customperpetualdailyweeklyoncerotation round-robin.
  • Default Fallbacks: getTaskInterval (server/services/taskSchedule.js:171) and updateTaskInterval (server/services/taskSchedule.js:182) fallback to rotation when intervals are unknown or unconfigured.

Perpetual vs On-Demand Coupling Today

  • Switching to Perpetual in GlobalConfigControls.jsx:285-310 writes { type: 'perpetual' } which overwrites and clears cronExpression.
  • Refill runs queue through on-demand (ON_DEMAND_ORIGINS.REFILL in server/services/taskScheduleConstants.js:22-25).
  • Drain candidate detection hardcodes special cases across isPerpetualRefillCandidate (cos.js:1466-1476), applyPerpetualDrainCap (cosTaskGenerator.js:2845-2849), and emitOnDemandEmpty (cosTaskGenerator.js:2579-2580): checking type === PERPETUAL or type === ON_DEMAND && isReconcileDrainTaskType(analysisType).

UI & API Surfaces Requiring Updates

  • Global Controls: client/src/components/cos/tabs/schedule/GlobalConfigControls.jsx:285-310, 717-726.
  • Per-App Overrides: client/src/components/cos/tabs/schedule/AppOverrideRow.jsx:133-140.
  • App Automation Tab: client/src/components/apps/tabs/AutomationTab.jsx:17-25.
  • Layered Intelligence Tab: client/src/components/apps/LayeredIntelligenceTab.jsx:181-186 (intervalFieldsFromMs).
  • Schedule Constants & Statuses: client/src/components/cos/tabs/schedule/scheduleConstants.js:5-25, 56-63, 68-85, 106-161.
  • Badge Component: client/src/components/cos/tabs/schedule/IntervalBadge.jsx.
  • Dashboard Widget: client/src/components/UpcomingTasksWidget.jsx:35-44.
  • Workflow & Calendar: server/services/workflow.js:459-470 and client/src/components/cos/tabs/WorkflowTab.jsx:18-40.
  • API Endpoints & Validation:
    • GET /api/cos/schedule/interval-types: server/routes/cosScheduleRoutes.js:240-255.
    • PUT Allowed Fields (SCHEDULE_FIELDS): server/routes/cosScheduleRoutes.js:24-32.
    • Per-App Allowlist: server/routes/apps/taskTypes.js:241-258.

Persistence & Migrations

  • Files on Disk: data/cos/task-schedule.json, data/task-schedule.json, and apps.json (taskTypeOverrides[taskType].interval / intervalMs).
  • Migration System: Scripts live in scripts/migrations/NNN-*.js. Latest applied migration on tree is 331-codex-text-transport.js. Next migration is 332-schedule-interval-types.js.
  • loadSchedule() in server/services/taskScheduleStore.js handles runtime loading and defaults merging.

Proposed Approach

1. Unified Cadence Model

Update server/services/taskScheduleConstants.js:

export const INTERVAL_TYPES = {
  ON_DEMAND: 'on-demand',
  CRON: 'cron'
};

// Retained for legacy normalizer / migration decoding only
export const LEGACY_INTERVAL_TYPES = {
  ROTATION: 'rotation',
  DAILY: 'daily',
  WEEKLY: 'weekly',
  ONCE: 'once',
  CUSTOM: 'custom',
  PERPETUAL: 'perpetual'
};

Task schedule objects store:

  • type: 'on-demand' | 'cron'
  • cronExpression: string | null
  • perpetual: boolean (default false)

Execution Behavior Matrix

type perpetual Behavior
on-demand false Manual trigger only via "Run Now". Never auto-queued by scheduler (shouldRun: false, reason on-demand-only).
on-demand true Drain-until-done backlog execution. When parked, rechecks according to recheckCron or recheckIntervalMs (default 24h). Run Now resets brakes.
cron false Clock-scheduled execution. Evaluates cronExpression slots and catch-up rules.
cron true A matching cron slot initiates a drain run. While work remains (unparked), task continues draining back-to-back regardless of subsequent cron ticks. Once parked, the same cronExpression gates the next drain attempt (no separate recheckCron required).

Unknown or unconfigured tasks default to on-demand instead of rotation.

2. Consolidate Drain Special Cases

Replace checks for type === PERPETUAL || isReconcileDrainTaskType(...) in isPerpetualRefillCandidate, applyPerpetualDrainCap, emitOnDemandEmpty, and getNextTaskType with an explicit check for Boolean(interval.perpetual).

Shipped defaults for branch-reconcile and issue-reconcile will be set to type: 'on-demand' with perpetual: true.

3. In-Place Migration and Runtime Normalization

  1. File Migration (scripts/migrations/332-schedule-interval-types.js):

    • Idempotently transform data/cos/task-schedule.json, data/task-schedule.json, and apps.json overrides.
    • Mapping rules:
      • oncetype: 'on-demand', perpetual: false.
      • dailytype: 'cron', cronExpression: '0 7 * * *'.
      • weeklytype: 'cron', cronExpression: '0 7 * * 1'.
      • rotationtype: 'cron', cronExpression: '0 7 * * *'.
      • perpetualtype: 'on-demand', perpetual: true (retaining recheckCron / recheckIntervalMs).
      • customtype: 'cron', generating cronExpression from intervalMs using the conversion helper below.
      • on-demandtype: 'on-demand' (gaining perpetual: true for branch-reconcile / issue-reconcile).
    • Per-app interval values matching legacy enums map to the same rules; raw 5-field cron strings remain intact.
  2. Numeric intervalMs to Cron Conversion Helper:

    • 15m (900000ms) → */15 * * * *
    • 30m (1800000ms) → */30 * * * *
    • 1h (3600000ms) → 0 * * * *
    • N * 1h (where N divides 24) → 0 */N * * *
    • 24h (86400000ms) → 0 7 * * *
    • 7d (604800000ms) → 0 7 * * 1
    • Other values round to nearest minute */M * * * * (M capped at 59) or hour 0 */H * * * (H capped at 23).
  3. Runtime Normalization:

    • loadSchedule() in server/services/taskScheduleStore.js will normalize legacy record shapes on read so un-migrated JSON payloads remain valid in memory and update cleanly on next save.

4. UI & Component Updates

  • GlobalConfigControls.jsx:
    • Interval Type select displays two options: On Demand (manual trigger only) (on-demand) and Scheduled (cron) (cron).
    • Selecting Scheduled displays CronInput (seeding cronExpression || '0 7 * * *').
    • Render an independent Perpetual checkbox below the cadence select.
    • Recheck cron input displays when type === 'on-demand' && perpetual === true. Hidden when type === 'cron' && perpetual === true.
    • Remove the legacy "Once Reset" button (GlobalConfigControls.jsx:717-726).
  • AppOverrideRow.jsx & AutomationTab.jsx:
    • Options updated to: Inherit, On Demand, Scheduled.
  • LayeredIntelligenceTab.jsx:
    • Update intervalFieldsFromMs to write the 5-field cron string as interval (per-app overrides already treat a space-containing string as cron — taskSchedule.js:846-847). Do not write the UI sentinel interval: 'cron', which only opens the editor and saves no cadence. Keep intervalMs if the numeric picker remains.
  • scheduleConstants.js & IntervalBadge.jsx:
    • Update INTERVAL_LABELS and INTERVAL_DESCRIPTIONS to reflect On-Demand and Scheduled.
    • describeNextRun checks config.perpetual boolean instead of config.type === 'perpetual'.
    • Render Perpetual as an additional status badge when config.perpetual is true.
    • Remove completed status group from STATUS_GROUPS and TASK_FILTERS.
  • UpcomingTasksWidget.jsx & WorkflowTab.jsx:
    • Update local label mapping and calendar projection logic for cron and perpetual flags.

5. API Routes & Validation

  • server/routes/cosScheduleRoutes.js:
    • Add 'perpetual' to SCHEDULE_FIELDS.
    • Update GET /api/cos/schedule/interval-types to return { ON_DEMAND: 'on-demand', CRON: 'cron' } and updated descriptions.
    • Update PUT body validation to accept on-demand and cron, with single-release auto-rewrite for legacy strings (rotation, daily, weekly, once, custom, perpetual).
  • server/routes/apps/taskTypes.js:
    • Update per-app interval validation to allow on-demand, 5-field cron strings, or null (with legacy fallback rewrite).

Acceptance Criteria

  • Global config, per-app override, and automation tab select controls display only On Demand and Scheduled options (plus Inherit on per-app dropdowns). Legacy types (Rotation, Daily, Weekly, Once, Perpetual) are removed from all pickers.
  • Perpetual is an independent toggle active for both On-Demand and Scheduled tasks.
  • A task configured as Scheduled + Perpetual initiates a drain on its cron schedule, drains until parked, and uses its cronExpression as the recheck schedule.
  • Migration 332-schedule-interval-types.js (and unit test) transforms existing schedule records in data/cos/task-schedule.json, data/task-schedule.json, and apps.json idempotently.
  • Un-migrated schedule files loaded at runtime are normalized cleanly by loadSchedule().
  • once tasks convert to on-demand; daily/weekly/rotation convert to 07:00 cron expressions; perpetual converts to on-demand with perpetual: true.
  • Default configurations for branch-reconcile and issue-reconcile are set to on-demand with perpetual: true.
  • Unknown tasks default to on-demand rather than rotation.
  • Layered Intelligence tab writes a 5-field cron string as the per-app interval (not daily/weekly/custom, and not the 'cron' editor sentinel).
  • Disabled tasks stay disabled. Already-cron tasks keep their expression. weekdaysOnly is preserved.
  • All unit and integration test suites (taskSchedule.test.js, cosScheduleRoutes.test.js, scheduleConstants.test.js, component tests) pass with updated models.
  • docs/API.md and related schedule documentation are updated to reflect the new API schemas and interval types.

Out of Scope

  • Modifying autonomous job recurrence schemas (createCosJobSchema, cronSchedule, recurrenceRuleSchema) or eventScheduler.
  • Per-app perpetual flag overrides (the perpetual toggle remains a task-level global property).
  • Altering drainDispatchCap, work-detectors, or refill origin logic outside of keying on interval.perpetual.
  • Federation or schemaVersions.js changes (schedules remain machine-local JSON).
  • Prompt-version bumps (PROMPT_VERSIONS / PREVIOUS_DEFAULT_PROMPTS).

Open Questions

None — decisions are locked:

  • rotation maps to 0 7 * * * (daily at 7 AM) as a conservative fallback because rotation is un-shipped in defaults and mapping to perpetual would cause excessive background drain.
  • weekly maps to 0 7 * * 1 (Mondays at 7 AM) to prevent automated work tasks from scheduling over weekend boundaries.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:cos-agentsChief-of-Staff autonomous agentseffort:highDispatch reasoning effort: highenhancementNew feature or requestmodel:mediumModel size: mediumplanTracked by /do:replanplanner:grok-configured-defaultPlan authored by the grok-configured-default modeluxProposed from a UX/design audit

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions