Class: Clacky::Server::Scheduler

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/server/scheduler.rb

Overview

Scheduler reads ~/.clacky/schedules.yml and runs tasks on a cron-like schedule.

It starts a background thread that ticks every 60 seconds, checks all configured schedules, and fires any task whose cron expression matches the current time.

Schedule file format (~/.clacky/schedules.yml):

- name: daily_report
task: daily_report          # references ~/.clacky/tasks/daily_report.md
cron: "0 9 * * 1-5"        # standard 5-field cron expression
enabled: true               # optional, defaults to true

Cron field order: minute hour day-of-month month day-of-week

Constant Summary collapse

SCHEDULES_FILE =
File.expand_path("~/.clacky/schedules.yml")
TASKS_DIR =
File.expand_path("~/.clacky/tasks")

Instance Method Summary collapse

Constructor Details

#initialize(session_registry:, session_builder:, task_runner:) ⇒ Scheduler

Returns a new instance of Scheduler.



26
27
28
29
30
31
32
33
34
35
36
# File 'lib/clacky/server/scheduler.rb', line 26

def initialize(session_registry:, session_builder:, task_runner:)
  @registry        = session_registry
  @session_builder = session_builder  # callable: (name:, working_dir:) -> session_id
  # Callable that runs a task on an agent with unified status/save/broadcast
  # handling — signature: (session_id, agent, &block). Same contract as
  # the one ChannelManager receives.
  @task_runner     = task_runner
  @thread          = nil
  @running         = false
  @mutex           = Mutex.new
end

Instance Method Details

#add_schedule(name:, task:, cron:, enabled: true) ⇒ Object

Add or update a schedule entry in schedules.yml.



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/clacky/server/scheduler.rb', line 69

def add_schedule(name:, task:, cron:, enabled: true)
  list = load_schedules
  # Remove existing entry with the same name
  list.reject! { |s| s["name"] == name }
  list << {
    "name"    => name,
    "task"    => task,
    "cron"    => cron,
    "enabled" => enabled
  }
  save_schedules(list)
end

#create_cron_task(name:, content:, cron:, enabled: true) ⇒ Object

Create a task file and its schedule in one step.



107
108
109
110
# File 'lib/clacky/server/scheduler.rb', line 107

def create_cron_task(name:, content:, cron:, enabled: true)
  write_task(name, content)
  add_schedule(name: name, task: name, cron: cron, enabled: enabled)
end

#delete_cron_task(name) ⇒ Object

Delete a cron-task: remove both the task file and its schedule.



121
122
123
124
125
# File 'lib/clacky/server/scheduler.rb', line 121

def delete_cron_task(name)
  removed_schedule = remove_schedule(name)
  removed_task     = delete_task(name)
  removed_schedule || removed_task
end

#delete_task(task_name) ⇒ Object

Delete a task file and remove all schedules that reference it. Returns true if the task file existed and was deleted, false otherwise.



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/clacky/server/scheduler.rb', line 173

def delete_task(task_name)
  path = task_file_path(task_name)
  return false unless File.exist?(path)

  File.delete(path)
  # Remove all schedules referencing this task
  load_schedules.select { |s| s["task"] == task_name }.each do |s|
    remove_schedule(s["name"])
  end
  true
end

#list_cron_tasksObject

Return a merged list of cron-tasks (task content + schedule metadata).



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/clacky/server/scheduler.rb', line 128

def list_cron_tasks
  schedule_map = load_schedules.each_with_object({}) do |s, h|
    h[s["task"]] = s if s.is_a?(Hash)
  end

  list_tasks.map do |task_name|
    content  = begin; read_task(task_name); rescue StandardError; ""; end
    schedule = schedule_map[task_name] || {}
    {
      "name"    => task_name,
      "content" => content,
      "cron"    => schedule["cron"],
      "enabled" => schedule.fetch("enabled", true),
      "scheduled" => !schedule.empty?
    }
  end
end

#list_tasksObject

List all existing task names.



163
164
165
166
167
168
169
# File 'lib/clacky/server/scheduler.rb', line 163

def list_tasks
  return [] unless Dir.exist?(TASKS_DIR)

  Dir.glob(File.join(TASKS_DIR, "*.md")).map do |path|
    File.basename(path, ".md")
  end.sort
end

#read_task(task_name) ⇒ Object

Read the prompt content of a named task.



149
150
151
152
153
154
# File 'lib/clacky/server/scheduler.rb', line 149

def read_task(task_name)
  path = task_file_path(task_name)
  raise "Task not found: #{task_name} (expected #{path})" unless File.exist?(path)

  File.read(path)
end

#remove_schedule(name) ⇒ Object

Remove a schedule entry by name.



83
84
85
86
87
88
89
# File 'lib/clacky/server/scheduler.rb', line 83

def remove_schedule(name)
  list = load_schedules
  before_count = list.size
  list.reject! { |s| s["name"] == name }
  save_schedules(list)
  list.size < before_count
end

#running?Boolean

Returns:

  • (Boolean)


57
58
59
# File 'lib/clacky/server/scheduler.rb', line 57

def running?
  @running
end

#schedulesObject

Return all schedules from the config file.



62
63
64
# File 'lib/clacky/server/scheduler.rb', line 62

def schedules
  load_schedules
end

#startObject

Start the background scheduler thread.



39
40
41
42
43
44
45
46
# File 'lib/clacky/server/scheduler.rb', line 39

def start
  @mutex.synchronize do
    return if @running

    @running = true
    @thread  = Clacky::ThreadRegistry.spawn(name: "clacky-scheduler") { run_loop }
  end
end

#stopObject

Stop the background scheduler thread gracefully. NOTE: intentionally avoids Mutex here so it is safe to call from a signal trap context (Ruby disallows Mutex#synchronize inside traps).



51
52
53
54
55
# File 'lib/clacky/server/scheduler.rb', line 51

def stop
  @running = false
  @thread&.wakeup rescue nil
  @thread&.join(5)
end

#task_file_path(task_name) ⇒ Object

Return the file path for a task.



186
187
188
# File 'lib/clacky/server/scheduler.rb', line 186

def task_file_path(task_name)
  File.join(TASKS_DIR, "#{task_name}.md")
end

#update_cron_task(name, content: nil, cron: nil, enabled: nil) ⇒ Object

Update a cron-task: optionally update content and/or schedule fields.



113
114
115
116
117
118
# File 'lib/clacky/server/scheduler.rb', line 113

def update_cron_task(name, content: nil, cron: nil, enabled: nil)
  raise "Cron task not found: #{name}" unless list_tasks.include?(name)

  write_task(name, content) unless content.nil?
  update_schedule(name, cron: cron, enabled: enabled) if cron || !enabled.nil?
end

#update_schedule(name, cron: nil, enabled: nil) ⇒ Object

Update an existing schedule entry (cron and/or enabled). Returns false if the schedule does not exist.



93
94
95
96
97
98
99
100
101
102
# File 'lib/clacky/server/scheduler.rb', line 93

def update_schedule(name, cron: nil, enabled: nil)
  list = load_schedules
  entry = list.find { |s| s["name"] == name }
  return false unless entry

  entry["cron"]    = cron    unless cron.nil?
  entry["enabled"] = enabled unless enabled.nil?
  save_schedules(list)
  true
end

#write_task(task_name, content) ⇒ Object

Write the prompt content for a named task.



157
158
159
160
# File 'lib/clacky/server/scheduler.rb', line 157

def write_task(task_name, content)
  FileUtils.mkdir_p(TASKS_DIR)
  File.write(task_file_path(task_name), content)
end