Class: AgentsControl::Anchors::Scheduler

Inherits:
Object
  • Object
show all
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

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

#startObject

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

#stopObject



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.message}")
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.

Returns:

  • (Boolean)


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