diffbroker 0.10.0 → 0.11.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: 90e8bd666f8efcb2f12429d70caee992e7e06337041a32333b334beedf0c6053
4
- data.tar.gz: 48507f0c813906471b8345e3473f8f7f5c729c4c68ebeb46ac45579f3b3ed87e
3
+ metadata.gz: 5edf0d3084adb3e7b1e8a9f55dda5062efa3a5207d8f4ca7d7860141acef3ec7
4
+ data.tar.gz: 85899028c0a4d827e3df60c530298f9713e4a33e4ccf5ec4397fae0d58a6c37a
5
5
  SHA512:
6
- metadata.gz: 58362a84f6cc68cde8a081c5f02d6177db48656557bb9262390b272357e574343c33153d2118728b8dd18971cb68962199d327d740cc342ca9490e54fbe55196
7
- data.tar.gz: 6d4ee8a4c7fdc7d8619ed4efc2caef559a354bc28f6987ebe213cb94122ba989ff4686a8ae6a03aec2e72c993318e3aca2a4cc99bb923495082d4493340dab1e
6
+ metadata.gz: 7b7ca4efd11472c69863a386ebf10336402eac96f998a8665457f20a7f6b395af3a3814925915e1881d0f018c906344a0eccd45cdd0d9557fc4f44fc0a3891be
7
+ data.tar.gz: d763d542d2a6c27adad2bf78cf3b6f0bb1488b0f384bb90fef6990b27c142af472078409f5886be43c92fe6df7b158e847547720018f28ce0efe57ab7060b4dc
@@ -16,6 +16,9 @@ module Diffbroker
16
16
  # The run's lines for the user's log: what the model said and the tools it called. Subclasses override.
17
17
  def parse_log(_line) = []
18
18
 
19
+ # Whether a chat message can continue this agent's earlier session (task["resume_session_id"]).
20
+ def resumes? = false
21
+
19
22
  def env(token, task_id) = { TOKEN_ENV => token, "DIFFBROKER_TASK_ID" => task_id }
20
23
 
21
24
  private
@@ -27,12 +27,17 @@ module Diffbroker
27
27
 
28
28
  def name = "claude_code"
29
29
 
30
+ def resumes? = true
31
+
30
32
  def autonomous_command(task, mcp_config:)
31
33
  args = task["read_only"] ? READ_ONLY_ARGS : @config.claude_args
32
- [ @config.claude_command, "-p", task.fetch("prompt"), "--output-format", "stream-json", "--verbose", *args,
34
+ [ @config.claude_command, "-p", task.fetch("prompt"), *resume(task), "--output-format", "stream-json", "--verbose", *args,
33
35
  "--mcp-config", mcp_config, *strict(task), "--plugin-dir", @plugin_dir ]
34
36
  end
35
37
 
38
+ # The permissions above are passed again: a resumed session keeps its conversation, not its sandbox.
39
+ def resume(task) = task["resume_session_id"] ? [ "--resume", task["resume_session_id"] ] : []
40
+
36
41
  def terminal_command(task, mcp_config:)
37
42
  [ @config.claude_command, "--mcp-config", mcp_config, *strict(task), "--plugin-dir", @plugin_dir ]
38
43
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Diffbroker
7
+ # The agent sessions this companion started itself. The server offers to resume a chat's earlier session by id, but it is
8
+ # only ever resumed here when this machine made it: a server (or anyone who could fake its message) cannot make an agent
9
+ # continue another conversation of the user's, such as an interactive session in their own terminal.
10
+ class SessionLedger
11
+ FILE = "sessions.json"
12
+ LIMIT = 500
13
+ ID = /\A\w[\w-]{0,99}\z/
14
+
15
+ def initialize(state_dir:)
16
+ @path = File.join(state_dir, FILE)
17
+ @lock = Mutex.new
18
+ end
19
+
20
+ def remember(id)
21
+ return unless id.to_s.match?(ID)
22
+
23
+ @lock.synchronize do
24
+ ids = read
25
+ next if ids.include?(id)
26
+
27
+ write((ids + [ id ]).last(LIMIT))
28
+ end
29
+ end
30
+
31
+ def known?(id) = id.to_s.match?(ID) && @lock.synchronize { read.include?(id) }
32
+
33
+ private
34
+
35
+ def read
36
+ data = JSON.parse(File.read(@path))
37
+ data.is_a?(Array) ? data.grep(String) : []
38
+ rescue Errno::ENOENT, JSON::ParserError
39
+ []
40
+ end
41
+
42
+ def write(ids)
43
+ FileUtils.mkdir_p(File.dirname(@path), mode: 0o700)
44
+ File.write(@path, JSON.generate(ids), perm: 0o600)
45
+ end
46
+ end
47
+ end
@@ -12,12 +12,12 @@ module Diffbroker
12
12
  PRINT_OUTPUT = 120
13
13
 
14
14
  Handle = Struct.new(:pid, :cancelled)
15
- Outcome = Struct.new(:status, :cost_usd, :error_code, :last_output)
15
+ Outcome = Struct.new(:status, :cost_usd, :error_code, :last_output, :no_result)
16
16
 
17
17
  def initialize(config:, report:, worktree: Worktree.new(root: config.worktree_root), neutralizer: Neutralizer.new,
18
18
  gh_check: GhCheck.new, launcher: Launcher.new(name: config.terminal), out: $stdout, grace: CANCEL_GRACE,
19
19
  progress_interval: PROGRESS_INTERVAL, run_script: RunScript.new(state_dir: config.state_dir),
20
- reaper: Reaper.new(root: config.worktree_root))
20
+ reaper: Reaper.new(root: config.worktree_root), sessions: SessionLedger.new(state_dir: config.state_dir))
21
21
  @config = config
22
22
  @report = report
23
23
  @worktree = worktree
@@ -29,6 +29,7 @@ module Diffbroker
29
29
  @progress_interval = progress_interval
30
30
  @run_script = run_script
31
31
  @reaper = reaper
32
+ @sessions = sessions
32
33
  @handles = {}
33
34
  @running = Set.new
34
35
  @cancelled = Set.new
@@ -132,7 +133,7 @@ module Diffbroker
132
133
  def run_autonomous(task, agent, mcp_config, dir)
133
134
  id = task["task_id"]
134
135
  @lock.synchronize { @running << id }
135
- outcome = run_process(task, agent, mcp_config, dir)
136
+ outcome = run_with_resume(task, agent, mcp_config, dir)
136
137
  outcome = verify_after_write(task, outcome, agent, mcp_config, dir) if ExplainVerifier.verified?(task) && outcome.status == "done"
137
138
  outcome = publish_branch(task, outcome, dir) if outcome.status == "done"
138
139
  finished(id, outcome.status, outcome.cost_usd, outcome.error_code, outcome.last_output)
@@ -143,6 +144,27 @@ module Diffbroker
143
144
  end
144
145
  end
145
146
 
147
+ # A chat message the server offered to resume (task["resume"]) continues the agent's earlier session, when this machine made it
148
+ # and this agent can: it gets only the new question and keeps what it read. A session that cannot be resumed (deleted, expired)
149
+ # makes the agent exit without a word: the message then runs again from scratch with the full prompt, which carries the chat.
150
+ def run_with_resume(task, agent, mcp_config, dir)
151
+ resume = resume_for(task, agent)
152
+ return run_process(task, agent, mcp_config, dir) unless resume
153
+
154
+ outcome = run_process(task.merge("prompt" => resume["prompt"], "resume_session_id" => resume["session_id"]), agent, mcp_config, dir)
155
+ return outcome unless outcome.status == "failed" && outcome.no_result
156
+
157
+ say(task["task_id"], "Could not resume the earlier session: starting this message from scratch")
158
+ run_process(task, agent, mcp_config, dir)
159
+ end
160
+
161
+ def resume_for(task, agent)
162
+ resume = task["resume"]
163
+ return unless resume.is_a?(Hash) && agent.resumes? && resume["agent"] == agent.name && resume["prompt"].to_s != ""
164
+
165
+ resume if @sessions.known?(resume["session_id"])
166
+ end
167
+
146
168
  # An update_branch task ends with a rebase the agent may not push (see BranchPublisher): the companion does, here.
147
169
  def publish_branch(task, outcome, dir)
148
170
  return outcome unless publishes_branch?(task)
@@ -197,7 +219,7 @@ module Diffbroker
197
219
 
198
220
  if handle.cancelled then Outcome.new("cancelled", result&.cost_usd, "cancelled", output)
199
221
  elsif status.success? && !result&.error then Outcome.new("done", result&.cost_usd, nil, output)
200
- else Outcome.new("failed", result&.cost_usd, "agent_failed", output)
222
+ else Outcome.new("failed", result&.cost_usd, "agent_failed", output, result.nil?)
201
223
  end
202
224
  ensure
203
225
  writer.close if writer && !writer.closed?
@@ -221,6 +243,10 @@ module Diffbroker
221
243
  case (event = agent.parse_stream(line))
222
244
  when Agents::Result then result = event
223
245
  when Agents::Progress
246
+ if event.session_id
247
+ @sessions.remember(event.session_id)
248
+ progress(id, event.step, nil, nil, event.session_id)
249
+ end
224
250
  next if event.step == last_step || monotonic - last_sent < @progress_interval
225
251
 
226
252
  progress(id, event.step)
@@ -256,8 +282,8 @@ module Diffbroker
256
282
  end
257
283
  end
258
284
 
259
- def progress(id, step, agent = nil, phase = nil)
260
- @report.call("progress", { "task_id" => id, "step" => step, "agent" => agent, "phase" => phase }.compact)
285
+ def progress(id, step, agent = nil, phase = nil, session_id = nil)
286
+ @report.call("progress", { "task_id" => id, "step" => step, "agent" => agent, "phase" => phase, "session_id" => session_id }.compact)
261
287
  say(id, "progress: #{step}") unless step == "starting"
262
288
  end
263
289
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Diffbroker
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
data/lib/diffbroker.rb CHANGED
@@ -39,6 +39,7 @@ module Diffbroker
39
39
  autoload :RunScript, File.expand_path("diffbroker/run_script", __dir__)
40
40
  autoload :Runner, File.expand_path("diffbroker/runner", __dir__)
41
41
  autoload :ServiceInstaller, File.expand_path("diffbroker/service_installer", __dir__)
42
+ autoload :SessionLedger, File.expand_path("diffbroker/session_ledger", __dir__)
42
43
  autoload :SkillInstaller, File.expand_path("diffbroker/skill_installer", __dir__)
43
44
  autoload :TaskFiles, File.expand_path("diffbroker/task_files", __dir__)
44
45
  autoload :TaskRunner, File.expand_path("diffbroker/task_runner", __dir__)
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "diffbroker",
3
- "version": "0.10.0",
3
+ "version": "0.11.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.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Sidorov (@newstler)
@@ -63,6 +63,7 @@ files:
63
63
  - lib/diffbroker/run_script.rb
64
64
  - lib/diffbroker/runner.rb
65
65
  - lib/diffbroker/service_installer.rb
66
+ - lib/diffbroker/session_ledger.rb
66
67
  - lib/diffbroker/skill_installer.rb
67
68
  - lib/diffbroker/task_files.rb
68
69
  - lib/diffbroker/task_runner.rb