{"version":3,"file":"scheduler-D9Mqp8B5.cjs","names":["MastraBase","RegisteredLogger","#schedulesStore","#pubsub","#config","#started","#stopping","#missingWorkflowCounts","#runTick","#intervalHandle","#inflightTick","#processTick","#fireSchedule","#ensureTargetReady","computeNextFireAt","#notifyError","#publishTargetStart"],"sources":["../src/workflows/scheduler/scheduler.ts"],"sourcesContent":["import { MastraBase } from '../../base';\nimport type { PubSub } from '../../events/pubsub';\nimport { RegisteredLogger } from '../../logger/constants';\nimport type { Schedule, ScheduleTrigger, SchedulesStorage } from '../../storage/domains/schedules/base';\nimport { computeNextFireAt } from './cron';\nimport type { SchedulerConfig } from './types';\n\nconst TOPIC_WORKFLOWS = 'workflows';\nexport const TOPIC_AGENT_SCHEDULES = 'agent-schedules';\nconst DEFAULT_TICK_INTERVAL_MS = 10_000;\nconst DEFAULT_BATCH_SIZE = 100;\nconst DEFAULT_MISSES_BEFORE_DELETE = 3;\n\n/**\n * Drives cron-based workflow triggers.\n *\n * On each tick the scheduler:\n *  1. Loads schedules whose `nextFireAt <= now` from storage.\n *  2. Computes the next fire time from the cron expression.\n *  3. Atomically advances `nextFireAt` via compare-and-swap. Only one\n *     instance across many polling the same storage can claim a fire.\n *  4. Publishes a `workflow.start` event on the `workflows` pubsub topic.\n *  5. Records the trigger in the schedule's history.\n *\n * The scheduler does **not** execute workflows. The existing\n * `WorkflowEventProcessor` consumes `workflow.start` events and runs them.\n */\nexport class Scheduler extends MastraBase {\n  #schedulesStore: SchedulesStorage;\n  #pubsub: PubSub;\n  #config: Required<Pick<SchedulerConfig, 'tickIntervalMs' | 'batchSize'>> & SchedulerConfig;\n\n  #intervalHandle?: ReturnType<typeof setInterval>;\n  #inflightTick?: Promise<void>;\n  #started = false;\n  #stopping = false;\n\n  /**\n   * Per-schedule count of consecutive ticks where the target workflow was\n   * not registered with the host Mastra instance. Reset when the workflow\n   * resolves or the schedule is deleted. Used to ride out deploy/startup\n   * ordering races before reclaiming a ghost row.\n   */\n  #missingWorkflowCounts = new Map<string, number>();\n\n  constructor({\n    schedulesStore,\n    pubsub,\n    config,\n  }: {\n    schedulesStore: SchedulesStorage;\n    pubsub: PubSub;\n    config?: SchedulerConfig;\n  }) {\n    super({ component: RegisteredLogger.WORKFLOW, name: 'Scheduler' });\n    this.#schedulesStore = schedulesStore;\n    this.#pubsub = pubsub;\n    this.#config = {\n      ...config,\n      tickIntervalMs: config?.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS,\n      batchSize: config?.batchSize ?? DEFAULT_BATCH_SIZE,\n    };\n  }\n\n  /** Start the periodic tick loop. Runs an immediate tick first. */\n  async start(): Promise<void> {\n    if (this.#started) return;\n    this.#started = true;\n    this.#stopping = false;\n    // Fresh process / fresh grace window — old miss counts shouldn't carry\n    // over into a new start() since the workflow registry may now look\n    // different.\n    this.#missingWorkflowCounts.clear();\n\n    try {\n      // Run one tick immediately so newly-due schedules don't wait the full interval.\n      await this.#runTick();\n\n      // If stop() ran concurrently with the warm-up tick, don't arm a new\n      // interval afterwards — the caller has already asked us to shut down.\n      if (this.#stopping || !this.#started) return;\n\n      this.#intervalHandle = setInterval(() => {\n        // Swallow rejections here so a tick failure can't surface as an\n        // unhandled promise rejection and crash the host process. #processTick\n        // already logs its own errors and notifies onError, so we only need a\n        // belt-and-braces logger.error for anything that escapes.\n        void this.#runTick().catch(err => {\n          this.logger.error('Scheduler tick crashed', { error: err });\n        });\n      }, this.#config.tickIntervalMs);\n\n      // Don't keep the process alive just because the scheduler is polling.\n      // The process should be able to exit when all other work is done.\n      // Without .unref(), the setInterval prevents clean shutdown in\n      // scripts that create a Mastra instance (which auto-creates the\n      // notification dispatch workflow with a cron schedule) and exit\n      // after a single agent.generate() call.\n      // Optional call: on runtimes where setInterval returns a number\n      // (e.g. Cloudflare Workers) there is no unref and nothing to release.\n      this.#intervalHandle.unref?.();\n    } catch (err) {\n      // Reset state so a future start() can retry. Without this, a failed\n      // warm-up tick would leave #started=true with no interval armed and\n      // every subsequent start() call would silently no-op.\n      this.#started = false;\n      this.#stopping = false;\n      throw err;\n    }\n  }\n\n  /** Stop the tick loop and wait for any in-flight tick to finish. */\n  async stop(): Promise<void> {\n    if (!this.#started) return;\n    this.#stopping = true;\n\n    if (this.#intervalHandle) {\n      clearInterval(this.#intervalHandle);\n      this.#intervalHandle = undefined;\n    }\n\n    if (this.#inflightTick) {\n      try {\n        await this.#inflightTick;\n      } catch {\n        // tick errors are already logged; swallow during shutdown\n      }\n    }\n\n    this.#started = false;\n    this.#stopping = false;\n  }\n\n  /** True when the scheduler is currently running its tick loop. */\n  get isRunning(): boolean {\n    return this.#started;\n  }\n\n  /**\n   * Run a single tick. Public for tests; production callers should rely\n   * on the interval started by `start()`.\n   */\n  async tick(): Promise<void> {\n    await this.#runTick();\n  }\n\n  // -------- Internals --------\n\n  async #runTick(): Promise<void> {\n    if (this.#stopping || this.#inflightTick) return;\n    const promise = this.#processTick().finally(() => {\n      this.#inflightTick = undefined;\n    });\n    this.#inflightTick = promise;\n    await promise;\n  }\n\n  async #processTick(): Promise<void> {\n    let due: Schedule[];\n    try {\n      due = await this.#schedulesStore.listDueSchedules(Date.now(), this.#config.batchSize);\n    } catch (err) {\n      this.logger.error('Failed to list due schedules', { error: err });\n      return;\n    }\n\n    for (const schedule of due) {\n      if (this.#stopping) break;\n      await this.#fireSchedule(schedule);\n    }\n  }\n\n  /**\n   * Check whether a schedule's target is registered with the host\n   * Mastra instance. Returns `true` if no predicate is configured (we can't\n   * verify, so assume the consumer will reject) or if the target resolves.\n   *\n   * When the target is missing, we increment an in-memory counter and\n   * delete the schedule after `missesBeforeDelete` consecutive misses. The\n   * grace window protects against deploy/startup ordering races where the\n   * scheduler ticks before workflows/agents finish registering on a fresh\n   * process. Returns `false` to tell `#fireSchedule` to skip publishing for\n   * this tick.\n   */\n  async #ensureTargetReady(schedule: Schedule): Promise<boolean> {\n    const predicate = this.#config.isTargetReady;\n    if (!predicate) return true;\n\n    if (predicate(schedule.target)) {\n      this.#missingWorkflowCounts.delete(schedule.id);\n      return true;\n    }\n\n    const targetSummary =\n      schedule.target.type === 'workflow'\n        ? { workflowId: schedule.target.workflowId }\n        : { agentId: schedule.target.agentId };\n\n    const limit = this.#config.missesBeforeDelete ?? DEFAULT_MISSES_BEFORE_DELETE;\n    const prev = this.#missingWorkflowCounts.get(schedule.id) ?? 0;\n    const next = prev + 1;\n\n    if (next < limit) {\n      this.#missingWorkflowCounts.set(schedule.id, next);\n      if (prev === 0) {\n        this.logger.warn('Schedule target is not registered; skipping until it appears', {\n          scheduleId: schedule.id,\n          targetType: schedule.target.type,\n          ...targetSummary,\n          missesBeforeDelete: limit,\n        });\n      }\n      return false;\n    }\n\n    // Hit the grace limit — reclaim the row.\n    this.logger.error('Deleting schedule whose target has not been registered', {\n      scheduleId: schedule.id,\n      targetType: schedule.target.type,\n      ...targetSummary,\n      consecutiveMisses: next,\n    });\n    try {\n      await this.#schedulesStore.deleteSchedule(schedule.id);\n    } catch (err) {\n      this.logger.error('Failed to delete ghost schedule', {\n        scheduleId: schedule.id,\n        targetType: schedule.target.type,\n        ...targetSummary,\n        error: err,\n      });\n      // Keep the counter so we try again next tick rather than reset and\n      // start the grace window over.\n      return false;\n    }\n    this.#missingWorkflowCounts.delete(schedule.id);\n    return false;\n  }\n\n  async #fireSchedule(schedule: Schedule): Promise<void> {\n    if (!(await this.#ensureTargetReady(schedule))) return;\n\n    const actualFireAt = Date.now();\n\n    let newNextFireAt: number;\n    try {\n      newNextFireAt = computeNextFireAt(schedule.cron, {\n        timezone: schedule.timezone,\n        after: actualFireAt,\n      });\n    } catch (err) {\n      this.logger.error('Failed to compute next fire time for schedule', {\n        scheduleId: schedule.id,\n        cron: schedule.cron,\n        error: err,\n      });\n      this.#notifyError(err, schedule.id);\n      return;\n    }\n\n    // Deterministic runId so concurrent ticks across processes derive the same id.\n    const runId = `sched_${schedule.id}_${schedule.nextFireAt}`;\n\n    let claimed = false;\n    try {\n      claimed = await this.#schedulesStore.updateScheduleNextFire(\n        schedule.id,\n        schedule.nextFireAt,\n        newNextFireAt,\n        actualFireAt,\n        runId,\n      );\n    } catch (err) {\n      this.logger.error('Failed to claim due schedule fire', {\n        scheduleId: schedule.id,\n        runId,\n        error: err,\n      });\n      this.#notifyError(err, schedule.id);\n      return;\n    }\n\n    if (!claimed) {\n      // Another instance won the race, the row was paused/disabled, or the\n      // expected nextFireAt no longer matches. Skip publishing.\n      return;\n    }\n\n    let triggerStatus: ScheduleTrigger['outcome'] = 'published';\n    let triggerError: string | undefined;\n\n    try {\n      await this.#publishTargetStart(schedule, runId);\n    } catch (err) {\n      triggerStatus = 'failed';\n      triggerError = err instanceof Error ? err.message : String(err);\n      this.logger.error('Failed to publish target.start for schedule', {\n        scheduleId: schedule.id,\n        runId,\n        targetType: schedule.target.type,\n        error: err,\n      });\n      this.#notifyError(err, schedule.id);\n    }\n\n    // For workflow targets we record the trigger now with the claim id —\n    // the workflow event processor will reuse the same runId. For\n    // agent targets the AgentScheduleWorker records the trigger itself\n    // after the agent run starts, so it can write the real agent runId.\n    if (schedule.target.type === 'workflow' || triggerStatus === 'failed') {\n      try {\n        await this.#schedulesStore.recordTrigger({\n          scheduleId: schedule.id,\n          runId,\n          scheduledFireAt: schedule.nextFireAt,\n          actualFireAt,\n          outcome: triggerStatus,\n          error: triggerError,\n          triggerKind: 'schedule-fire',\n        });\n      } catch (err) {\n        this.logger.error('Failed to record schedule trigger', {\n          scheduleId: schedule.id,\n          runId,\n          error: err,\n        });\n      }\n    }\n  }\n\n  /**\n   * Invoke the user-supplied onError hook in isolation. A throwing hook\n   * must not abort the scheduler tick loop, so we swallow + log any error\n   * the callback itself raises.\n   */\n  #notifyError(error: unknown, scheduleId: string): void {\n    if (!this.#config.onError) return;\n    try {\n      this.#config.onError(error, { scheduleId });\n    } catch (callbackError) {\n      this.logger.error('Scheduler onError handler threw', {\n        scheduleId,\n        error: callbackError,\n      });\n    }\n  }\n\n  async #publishTargetStart(schedule: Schedule, claimId: string): Promise<void> {\n    switch (schedule.target.type) {\n      case 'workflow': {\n        const { workflowId, inputData, initialState, requestContext } = schedule.target;\n        await this.#pubsub.publish(TOPIC_WORKFLOWS, {\n          type: 'workflow.start',\n          runId: claimId,\n          data: {\n            workflowId,\n            runId: claimId,\n            prevResult: { status: 'success', output: inputData ?? {} },\n            requestContext: requestContext ?? {},\n            initialState: initialState ?? {},\n          },\n        });\n        return;\n      }\n      case 'agent': {\n        await this.#pubsub.publish(TOPIC_AGENT_SCHEDULES, {\n          type: 'agent-schedule.fire',\n          runId: claimId,\n          data: {\n            scheduleId: schedule.id,\n            claimId,\n            scheduledFireAt: schedule.nextFireAt,\n            target: schedule.target,\n          },\n        });\n        return;\n      }\n      default: {\n        throw new Error(`Unsupported schedule target type: ${(schedule.target as { type: string }).type}`);\n      }\n    }\n  }\n}\n\n/**\n * @deprecated Renamed to {@link Scheduler}. The scheduler now drives both\n * workflow and agent schedules, so the `Workflow`-prefixed name is no longer\n * accurate. This alias will be removed in a future major release.\n */\nexport const WorkflowScheduler = Scheduler;\n\n/**\n * @deprecated Renamed to {@link Scheduler}. This alias will be removed in a\n * future major release.\n */\nexport type WorkflowScheduler = Scheduler;\n"],"mappings":"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"}