Skip to content

[Audit] MEDIUM: Race condition in scheduler initialization #8

@NickCalabs

Description

@NickCalabs

Severity: MEDIUM

Description

The scheduler initialization in src/scheduler.ts:89-95 and src/server.ts:177 has a potential race condition where cron tasks may fire before the database and tools are fully initialized.

Location

File: src/server.ts
Lines: 175-178

Problematic Code

export async function startServer(): Promise<void> {
  const config = loadConfig();
  getDb(); // initialize database on startup
  await initTools();
  initScheduler();  // <-- Could fire tasks immediately
  const server = serve(...);
}

File: src/scheduler.ts
Lines: 52-66

const task = cron.schedule(expr, async () => {
  try {
    console.log(`Cron fired for agent "${agentName}" (cron: ${expr})`);
    const { createRun } = await import("./traces.ts");
    const { runAgent } = await import("./runner.ts");
    const runId = createRun(agentName);
    await runAgent(agentName, runId, `Scheduled run (cron: ${expr})`);
  } catch (err: unknown) {
    // ...
  }
});

Issues

  1. Immediate Execution Risk: If a cron expression is "* * * * *" (every minute) and we start at :59 seconds, the task fires within 1 second
  2. Tools Not Ready: MCP servers may still be connecting when first cron fires
  3. Dynamic Imports: Using import() inside the handler is slower and could fail
  4. No Startup Delay: No grace period after server start before scheduled tasks run

Impact

  • First scheduled run could fail with "tools not initialized"
  • Race between initTools() promise and cron task execution
  • Confusing error messages on startup
  • Flaky behavior that only appears with specific cron timings

Recommendation

Option 1: Add startup delay to scheduler:

export function initScheduler(delayMs: number = 5000): void {
  const agents = listAgents();
  
  // Schedule with delay to allow initialization to complete
  setTimeout(() => {
    for (const agent of agents) {
      const exprs = parseCronTriggers(agent.triggers);
      if (exprs.length > 0) {
        scheduleAgent(agent.name, agent.triggers);
      }
    }
    const count = scheduled.size;
    if (count > 0) {
      console.log(`Scheduler: ${count} agent(s) with cron triggers`);
    }
  }, delayMs);
}

Option 2: Make initScheduler async and await tools:

export async function startServer(): Promise<void> {
  const config = loadConfig();
  getDb();
  await initTools();
  await new Promise(resolve => setTimeout(resolve, 1000)); // grace period
  initScheduler();
  const server = serve(...);
}

Option 3: Check readiness in task handler:

const task = cron.schedule(expr, async () => {
  // Wait for tools to be ready
  const tools = listTools();
  if (tools.length === 0) {
    console.warn(`Cron task for ${agentName} skipped: tools not initialized yet`);
    return;
  }
  
  // ... rest of handler
});

Related Issues

  • No check for duplicate agent runs (could schedule same agent twice)
  • updateNextRun called in finally might race with next cron fire

Created by security audit

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions