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:
- The job increments its
retryCount. - If
retryCountexceedsmaxRetries(default 3), the job is skipped, logs askippedstatus, and schedules the next run (lines 99-108). - 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):
- Task Retrieval: Obtains the AI agent task via
WorkerManage. - Yolo Mode: Forces
yoloMode: trueto bypass interactive approval dialogs (lines 30-33). - File Handling: Calls
copyFilesToDirectorywith an empty array for cron jobs (lines 83-86). - Message Dispatch: Invokes
task.sendMessagewith the appropriate payload (contentfor Codex/ACP,inputfor Gemini) (lines 88-92). - State Update: Sets
lastStatus = 'ok', resetsretryCount, and updates the conversation'smodifyTimeto 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:
- Stops stale timers.
- Detects missed executions where
nextRunAtMs <= now. - Logs warnings and inserts a UI tips message via
addMessage, emittingipcBridge.conversation.responseStreamto notify the user instantly. - 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.tsprovides 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.tsprevents 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
powerSaveBlockerto prevent OS suspension during scheduled tasks, whilehandleSystemResumedetects 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 usingcroner,setInterval, orsetTimeout.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →