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.



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

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.



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

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.



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

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.



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

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).



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

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.



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

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.



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

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.



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

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)


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

def running?
  @running
end

#schedulesObject

Return all schedules from the config file.



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

def schedules
  load_schedules
end

#startObject

Start the background scheduler thread.



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

def start
  @mutex.synchronize do
    return if @running

    @running = true
    @thread  = Thread.new { run_loop }
    @thread.name = "clacky-scheduler"
  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).



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

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.



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

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.



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

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.



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

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.



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

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