diffbroker 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bdc96c23de6492a2ea0660c7f897c8c04efc4ea3993cf96e9b4c6bf57385644
4
- data.tar.gz: b1159b4577c725eb8f9a92277ccdfd84fcdf9cf655d09dbe384d70d6cc00bc75
3
+ metadata.gz: ca48a356eaffc2d3549190a987dfc0ee851b76a2c622d256c154cf17ae41d7d8
4
+ data.tar.gz: 436612e9a7695e506926430051c0b023b3e920ae4dd35b1de3c614df12155358
5
5
  SHA512:
6
- metadata.gz: 07e7a17ef7f21c5350e7796d58b75b5255d51a7fcac5ca7f2036e86f82915f4bfaa96a2058572667279e3ffe50d85953ad224977bfbf5da50fc88eec150aa6d5
7
- data.tar.gz: 7457ee92b3fbad1376448ae6be16e700a74ca4e7427a5f58aea9e713a4b09da88d4424f85201b6c95be8f1ba5f84e558f976fc570b8658a7110572e9750b0cee
6
+ metadata.gz: 46d637259952a1158a9429e07cabd21d14e35c36ef81e21786df3c071181f035790bb204286ea1822c3c96368f9e86c9eb08b911b5a33661f6528545d2420b3c
7
+ data.tar.gz: 2b9654e4d22fb4e127d62abb4ccaab62a3db448dceb0f932858bf9d6f82bedaaf7cdef4c2c57d027e4b5b60dfd42c419dadbc030bbd59db78fe5927f1f964cdd
@@ -42,7 +42,7 @@ module Diffbroker
42
42
  task_runner_factory: ->(report) { TaskRunner.new(config:, report:, out:) }, out: Stamped.new(out), relay:, process_scanner:, agent: config.agent,
43
43
  plan_usage: PlanUsage.new(agent: config.agent),
44
44
  lifecycle: Lifecycle.new(dir: config.dir, server: config.server), reaper: Reaper.new(root: config.worktree_root),
45
- state: DaemonState.new(dir: config.dir), lock: DaemonLock.new(dir: config.dir), server: config.server,
45
+ state: DaemonState.new(dir: config.dir), lock: DaemonLock.new(dir: config.dir), server: config.server, sleep_lock: SleepLock.new,
46
46
  rejection_confirmed: -> { server_rejects?(config) }, jitter: ->(seconds) { seconds * rand(0.8..1.25) })
47
47
  end
48
48
 
@@ -82,8 +82,9 @@ module Diffbroker
82
82
  def initialize(scanner:, client_factory:, task_runner_factory:, out: $stdout, sleeper: ->(seconds) { sleep(seconds) },
83
83
  clock: -> { Time.now }, spawner: ->(&block) { Thread.new(&block) }, relay: nil, process_scanner: nil, lifecycle: nil,
84
84
  maintenance: Maintenance.new, roots_changed: nil, device_roots: [], ignored_roots: [], agent: "claude", reaper: nil, plan_usage: nil,
85
- state: nil, lock: nil, rejection_confirmed: nil, server: nil, jitter: ->(seconds) { seconds })
85
+ state: nil, lock: nil, rejection_confirmed: nil, server: nil, jitter: ->(seconds) { seconds }, sleep_lock: nil)
86
86
  @scanner = scanner
87
+ @sleep_lock = sleep_lock
87
88
  @device_roots = device_roots
88
89
  @ignored_roots = ignored_roots
89
90
  @roots_changed = roots_changed
@@ -150,6 +151,7 @@ module Diffbroker
150
151
  ensure
151
152
  if @owner
152
153
  @task_runner.interrupt_all! if @task_runner.respond_to?(:interrupt_all!)
154
+ @sleep_lock&.release!
153
155
  @relay&.stop
154
156
  @lifecycle&.clear_pid
155
157
  @state&.clear(pid: Process.pid) unless @rejected # a rejected daemon leaves its reason for `status`
@@ -367,12 +369,14 @@ module Diffbroker
367
369
  return unless @running_lock.synchronize { !@upgrading && @running.add?(id) } # the upgrade's restart would kill it
368
370
 
369
371
  @state&.update(busy_tasks: @running_lock.synchronize { @running.size })
372
+ @sleep_lock&.acquire! # the machine may not fall asleep under a running agent; it sleeps as usual when the last one ends
370
373
  @spawner.call do
371
374
  @task_runner.run(message, repositories: known_repositories)
372
375
  rescue StandardError => e
373
376
  @out.puts "Task #{id} crashed: #{e.class}: #{e.message}"
374
377
  ensure
375
378
  upgrade_now = @running_lock.synchronize { @running.delete(id) && @pending_upgrade && @running.empty? }
379
+ @sleep_lock&.release! if @running_lock.synchronize { @running.empty? }
376
380
  @state&.update(busy_tasks: @running_lock.synchronize { @running.size })
377
381
  upgrade if upgrade_now
378
382
  sweep_strays
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffbroker
4
+ # Keeps the machine from going to sleep on its own while an agent task runs, and only then: the daemon takes the lock
5
+ # when its first task starts and gives it back when the last one ends, so the computer sleeps as usual the rest of the
6
+ # time. It stops idle sleep only; closing a laptop's lid, the power button or Apple menu > Sleep still sleep it.
7
+ #
8
+ # macOS: `caffeinate -i`. Linux: `systemd-inhibit` around a shell that waits for the daemon. Both end by themselves when
9
+ # the daemon dies, so a crash never leaves the machine awake. Without the tool, or when it cannot start, nothing happens.
10
+ class SleepLock
11
+ def initialize(platform: Diffbroker.platform, pid: Process.pid, available: method(:on_path?), spawner: Process.method(:spawn),
12
+ signal: Process.method(:kill), detacher: Process.method(:detach))
13
+ @platform = platform
14
+ @pid = pid
15
+ @available = available
16
+ @spawner = spawner
17
+ @signal = signal
18
+ @detacher = detacher
19
+ @child = nil
20
+ end
21
+
22
+ def held? = !@child.nil?
23
+
24
+ # True when the machine is being kept awake.
25
+ def acquire!
26
+ return true if held?
27
+
28
+ command = self.command
29
+ return false unless command && @available.call(command.first)
30
+
31
+ @child = @spawner.call(*command, in: File::NULL, out: File::NULL, err: File::NULL)
32
+ @detacher.call(@child)
33
+ true
34
+ rescue SystemCallError
35
+ @child = nil
36
+ false
37
+ end
38
+
39
+ def release!
40
+ child, @child = @child, nil
41
+ @signal.call("TERM", child) if child
42
+ rescue SystemCallError
43
+ nil
44
+ end
45
+
46
+ private
47
+
48
+ def command
49
+ case @platform
50
+ when "macos" then [ "caffeinate", "-i", "-w", @pid.to_s ]
51
+ when "linux"
52
+ [ "systemd-inhibit", "--what=sleep:idle", "--who=Diffbroker", "--why=An agent task is running", "--mode=block",
53
+ "sh", "-c", "while kill -0 #{@pid} 2>/dev/null; do sleep 5; done" ]
54
+ end
55
+ end
56
+
57
+ def on_path?(name)
58
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { File.executable?(File.join(_1, name)) }
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Diffbroker
4
- VERSION = "0.12.3"
4
+ VERSION = "0.13.0"
5
5
  end
data/lib/diffbroker.rb CHANGED
@@ -43,6 +43,7 @@ module Diffbroker
43
43
  autoload :Runner, File.expand_path("diffbroker/runner", __dir__)
44
44
  autoload :ServiceInstaller, File.expand_path("diffbroker/service_installer", __dir__)
45
45
  autoload :SessionLedger, File.expand_path("diffbroker/session_ledger", __dir__)
46
+ autoload :SleepLock, File.expand_path("diffbroker/sleep_lock", __dir__)
46
47
  autoload :SkillInstaller, File.expand_path("diffbroker/skill_installer", __dir__)
47
48
  autoload :TaskFiles, File.expand_path("diffbroker/task_files", __dir__)
48
49
  autoload :TaskRunner, File.expand_path("diffbroker/task_runner", __dir__)
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "diffbroker",
3
- "version": "0.12.3",
3
+ "version": "0.13.0",
4
4
  "description": "Skills for working on Diff Broker agent tasks: the diffbroker MCP tools, write rules and report_task."
5
5
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diffbroker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.3
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Sidorov (@newstler)
@@ -68,6 +68,7 @@ files:
68
68
  - lib/diffbroker/service_installer.rb
69
69
  - lib/diffbroker/session_ledger.rb
70
70
  - lib/diffbroker/skill_installer.rb
71
+ - lib/diffbroker/sleep_lock.rb
71
72
  - lib/diffbroker/task_files.rb
72
73
  - lib/diffbroker/task_runner.rb
73
74
  - lib/diffbroker/url_handler_installer.rb