Class: AgentsControl::Anchors::Scheduler
- Inherits:
-
Object
- Object
- AgentsControl::Anchors::Scheduler
- Defined in:
- lib/agents_control/anchors/scheduler.rb
Overview
Placing rate-limit windows on a schedule.
A five-hour window starts at the minute of the first message and expires exactly three hundred minutes later. This tool doesn't add a single extra token — it moves window boundaries to where they're convenient. The difference is between "the window reset at 2:37pm, mid-work" and "windows at exactly 7am, noon, 5pm."
Pinging has to use a cheap model. The five-hour window is shared across the account, but weekly limits are tracked per model family: an anchor on opus would spend the scarcest bucket for an effect haiku gives for free.
Constant Summary collapse
- DAYS =
%w[sun mon tue wed thu fri sat].freeze
- GRACE =
How late it's still worth catching up on a missed slot. If the laptop slept and woke an hour later, an anchor is already pointless — the window will start somewhere other than planned regardless.
300- TICK =
How often to wake up and check the clock.
30- ACTIVITY_KEY =
"agent:last_activity"- WINDOW =
5 * 3600
Instance Method Summary collapse
-
#initialize(config:, store:, executor: Executor.new, clock: -> { Time.now }, logger: nil) ⇒ Scheduler
constructor
A new instance of Scheduler.
-
#next_run_at(from = @clock.call) ⇒ Object
The next firing time — for doctor and /status.
-
#start ⇒ Object
The thread always starts; whether it's enabled is checked on every tick — so a setting changed from the menu takes effect without restarting the daemon.
- #stop ⇒ Object
-
#tick(now = @clock.call) ⇒ Object
One pass: fire if the time has come.
-
#window_active?(now = @clock.call) ⇒ Boolean
Whether a window is currently active.
Constructor Details
#initialize(config:, store:, executor: Executor.new, clock: -> { Time.now }, logger: nil) ⇒ Scheduler
Returns a new instance of Scheduler.
30 31 32 33 34 35 36 37 |
# File 'lib/agents_control/anchors/scheduler.rb', line 30 def initialize(config:, store:, executor: Executor.new, clock: -> { Time.now }, logger: nil) @config = config @store = store @executor = executor @clock = clock @logger = logger @running = false end |
Instance Method Details
#next_run_at(from = @clock.call) ⇒ Object
The next firing time — for doctor and /status.
66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/agents_control/anchors/scheduler.rb', line 66 def next_run_at(from = @clock.call) (0..7).each do |offset| date = from.to_date + offset next unless enabled_day?(date) slot = slots_on(date).find { |time| time > from } return slot if slot end nil end |
#start ⇒ Object
The thread always starts; whether it's enabled is checked on every tick — so a setting changed from the menu takes effect without restarting the daemon.
42 43 44 45 46 47 48 49 |
# File 'lib/agents_control/anchors/scheduler.rb', line 42 def start @running = true @thread = Thread.new do tick while @running end self end |
#stop ⇒ Object
51 52 53 54 |
# File 'lib/agents_control/anchors/scheduler.rb', line 51 def stop @running = false @thread&.kill end |
#tick(now = @clock.call) ⇒ Object
One pass: fire if the time has come.
57 58 59 60 61 62 63 |
# File 'lib/agents_control/anchors/scheduler.rb', line 57 def tick(now = @clock.call) due_slot(now)&.then { |slot| fire(slot, now) } if enabled? rescue StandardError => e log("failure: #{e.class}: #{e.}") ensure sleep(TICK) if @running end |
#window_active?(now = @clock.call) ⇒ Boolean
Whether a window is currently active. Known because the daemon sees every agent event: any of them means a human was just working.
80 81 82 83 84 85 |
# File 'lib/agents_control/anchors/scheduler.rb', line 80 def window_active?(now = @clock.call) last = @store.get(ACTIVITY_KEY) return false unless last now.to_i - last.to_i < WINDOW end |