How AionUi's CronService Schedules and Executes AI Tasks Securely with CronBusyGuard

AionUi's CronService uses a combination of SQLite-backed job persistence, multiple timer strategies, and the CronBusyGuard state tracker to ensure AI tasks execute only when target conversations are idle, preventing race conditions and duplicate responses.

AionUi is an open-source AI desktop application that automates agent interactions using scheduled tasks. The CronService in src/process/services/cron/CronService.ts provides the core scheduling engine, while CronBusyGuard in src/process/services/cron/CronBusyGuard.ts acts as a safety mechanism to block execution during active conversations. Together, they enable reliable, non-intrusive automation of Claude, Gemini, and Codex agents.

CronService Architecture and Lifecycle

The CronService initializes by loading enabled jobs from the SQLite-backed CronStore and activating their timers. During shutdown or job deletion, it cancels these timers and persists the final state.

In CronService.init(), the service queries all enabled jobs and invokes startTimer(job) for each entry. This method, located at lines 49-62 in src/process/services/cron/CronService.ts, ensures that scheduled tasks resume correctly after application restarts.

Scheduling Jobs and Timer Types

Jobs are created via cronService.addJob(params), defined at lines 73-106. This method enforces a one-job-per-conversation rule (lines 74-80) to prevent scheduling conflicts, computes the first nextRunAtMs using updateNextRunTime(job), persists the job to CronStore, and starts the appropriate timer.

The startTimer(job) method (lines 121-127) supports three scheduling strategies:

Cron Expressions

For kind: 'cron', the service uses the croner library to parse expressions like 0 9 * * *. The implementation at lines 200-216 stores the next run time via timer.nextRun() and updates state.nextRunAtMs at lines 178-181.

Fixed Intervals

For kind: 'every', the service uses setInterval (lines 226-232), calculating the next run as Date.now() + everyMs.

One-Off Delays

For kind: 'at', the service uses setTimeout with the delay computed from schedule.atMs (lines 240-259).

Preventing Race Conditions with CronBusyGuard

The CronBusyGuard ensures that scheduled AI tasks do not interrupt ongoing conversations. Located in src/process/services/cron/CronBusyGuard.ts, it maintains a Map<string, ConversationState> where each state tracks isProcessing and the last activity timestamp (lines 10-13).

Tracking Conversation State

When a conversation starts processing a message, the MessageMiddleware or renderer process calls cronBusyGuard.setProcessing(conversationId, true). Upon completion, it sets the flag to false. This real-time tracking prevents the cron service from injecting automated messages while a user is actively interacting with the agent.

Execution Guard and Retry Logic

Before executing a job, CronService.executeJob(job) checks cronBusyGuard.isProcessing(conversationId) (lines 95-98). If the conversation is busy:

  1. The job increments its retryCount.
  2. If retryCount exceeds maxRetries (default 3), the job is skipped, logs a skipped status, and schedules the next run (lines 99-108).
  3. Otherwise, the service registers a 30-second back-off timer using setTimeout (lines 111-117).

This mechanism ensures that automated tasks yield to user-initiated interactions while maintaining schedule integrity through intelligent retry policies.

Executing AI Tasks

When the conversation is idle, executeJob proceeds through the following steps (lines 88-101):

  1. Task Retrieval: Obtains the AI agent task via WorkerManage.
  2. Yolo Mode: Forces yoloMode: true to bypass interactive approval dialogs (lines 30-33).
  3. File Handling: Calls copyFilesToDirectory with an empty array for cron jobs (lines 83-86).
  4. Message Dispatch: Invokes task.sendMessage with the appropriate payload (content for Codex/ACP, input for Gemini) (lines 88-92).
  5. State Update: Sets lastStatus = 'ok', resets retryCount, and updates the conversation's modifyTime to bubble it to the top of the UI (lines 94-101).

Errors during execution set lastStatus = 'error' and store the error message before proceeding to the next scheduled run.

System Resilience and Power Management

Preventing OS Suspension

While any job is enabled, CronService activates Electron's powerSaveBlocker in prevent-app-suspension mode (lines 332-341). This prevents the operating system from suspending the application while tasks are pending, without keeping the display awake. When no jobs remain enabled, the blocker is stopped to conserve battery.

Handling System Resume

When the system wakes from sleep, handleSystemResume() (lines 559-587) iterates over all enabled jobs:

  1. Stops stale timers.
  2. Detects missed executions where nextRunAtMs <= now.
  3. Logs warnings and inserts a UI tips message via addMessage, emitting ipcBridge.conversation.responseStream to notify the user instantly.
  4. Restarts timers for future executions.

This ensures users are informed of any missed AI tasks due to system sleep, maintaining transparency in automated workflows.

Practical Code Examples

Adding a Daily Cron Job

import { cronService } from '@process/services/cron/CronService';
import type { CronSchedule } from '@process/services/cron/CronStore';

const dailyCron: CronSchedule = {
  kind: 'cron',
  expr: '0 9 * * *',      // 09:00 every day
  tz: 'Asia/Shanghai',
};

await cronService.addJob({
  name: 'Daily Summary',
  schedule: dailyCron,
  message: 'Please generate the daily summary.',
  conversationId: 'conv_12345',
  agentType: 'codex',
  createdBy: 'user',
});

Manually Guarding a Conversation

import { cronBusyGuard } from '@process/services/cron/CronBusyGuard';

function onConversationStart(id: string) {
  cronBusyGuard.setProcessing(id, true);
}

function onConversationEnd(id: string) {
  cronBusyGuard.setProcessing(id, false);
}

Listening for Missed Job Notifications

ipcBridge.conversation.responseStream.on('data', (msg) => {
  if (msg.type === 'tips' && msg.data.type === 'warning') {
    // Show a toast or banner with msg.data.content
  }
});

Summary

  • CronService in src/process/services/cron/CronService.ts provides the core scheduling engine for AionUi, supporting cron expressions, fixed intervals, and one-off delays via SQLite persistence.
  • CronBusyGuard in src/process/services/cron/CronBusyGuard.ts prevents race conditions by tracking per-conversation processing states, ensuring scheduled tasks only run when conversations are idle.
  • The service implements a retry policy with a 30-second back-off and maximum 3 attempts before skipping a run, preventing infinite loops on busy conversations.
  • Power management features use Electron's powerSaveBlocker to prevent OS suspension during scheduled tasks, while handleSystemResume detects and notifies users of missed executions after system sleep.
  • Jobs are created via addJob(), which enforces one-job-per-conversation rules and automatically starts the appropriate timer using croner, setInterval, or setTimeout.

Frequently Asked Questions

How does CronBusyGuard prevent duplicate AI responses?

CronBusyGuard maintains a Map of conversation states in src/process/services/cron/CronBusyGuard.ts. When a conversation starts processing any message, setProcessing(conversationId, true) marks it as busy. Before executing a scheduled job, CronService.executeJob() calls isProcessing() to check this state. If busy, the job defers execution using a 30-second retry timer, preventing the cron task from interrupting active conversations or causing duplicate responses.

What happens to scheduled tasks when my computer goes to sleep?

When the system resumes, CronService.handleSystemResume() (lines 559-587) iterates through all enabled jobs, stops stale timers, and compares nextRunAtMs against the current time. If a job was missed during sleep, it logs a warning and inserts a UI tip message via addMessage, emitting ipcBridge.conversation.responseStream to notify the user immediately. The service then restarts timers for future executions, ensuring no silent failures occur due to system sleep.

Can I schedule multiple cron jobs for the same conversation?

No. The addJob() method in src/process/services/cron/CronService.ts enforces a one-job-per-conversation rule at lines 74-80. If you attempt to create a job for a conversationId that already has an active scheduled task, the service throws an error. This design prevents scheduling conflicts and ensures that CronBusyGuard can accurately track conversation states without ambiguity from multiple concurrent automated workflows.

What is "yolo mode" and why do cron jobs use it?

Yolo mode is an execution flag that bypasses interactive approval dialogs for AI agents. In src/process/services/cron/CronService.ts (lines 30-33), cron jobs automatically set yoloMode: true when retrieving tasks via WorkerManage. This ensures that scheduled AI tasks execute without requiring user interaction, which is essential for unattended automation. The flag allows the cron service to send messages to Claude, Gemini, or Codex agents automatically while maintaining security through the CronBusyGuard rather than manual approvals.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →