lemans 1.3.1 → 1.3.2

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: 94b9194a7931b789ba1cb675cfc8d81e0367f8b73754ce1fa11cbff33fff7174
4
- data.tar.gz: 4b61d46c323ddd60cb016402d757cb53db56c0adb372709d73ebeec369789e41
3
+ metadata.gz: db9e22f2b2098371943cde85a366d70ff205b267719b67974484c5240752cd59
4
+ data.tar.gz: 707a10355b3fb212740b2839b51a1b346e8e6165a4aed5403f9c763045ffa6b8
5
5
  SHA512:
6
- metadata.gz: db72821e26aa89d9bb9922134042e38c3041a2a8df32669c7e3d69d9ad5f06ebd2fddbfcee89b0f38bc4a4a0f263190858fc24ee86aac5e51984ee1b62a7a6b1
7
- data.tar.gz: 1b144ff16c02f963031bf078f03f465508edad3f0ed061e0879905ca189143ad14e6a008a0f9fbaf2d0bea28e2124f82f84eea3f5596bb771251c868e675cd50
6
+ metadata.gz: f1472766d86a65544e6b525124fcd0afb828fa36c6a3c5ee1d4bb2d9f24972edb2d844abfdfc03735c6a9cd81b6bbda00b791f782b42cb7d523becdd01f764f1
7
+ data.tar.gz: 95d42dbaf57d92ace03b1bffdcf05316fd7244a5abb3fb74a5611ea15a4c8de4bdd86d6e9ab21b2b4bcf4606bce5ab11e5bf44fb372ae74c810d4c38b1c1b465
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.2] - 2026-09-08
4
+
5
+ - Miniswen: increase provider error max retry window to ~1 min.
6
+ - Collect patches on agent errors.
7
+
3
8
  ## [1.3.1] - 2026-09-04
4
9
 
5
10
  - `agent.max_output_tokens` and `lemans run --max-output-tokens`
data/exe/lemans-remote CHANGED
@@ -31,6 +31,7 @@
31
31
  # Then watch, fetch, and clean up:
32
32
  #
33
33
  # exe/lemans-remote status [--history] [--running | --complete] [-W [INTERVAL]]
34
+ # exe/lemans-remote logs RUN_ID [--tail N]
34
35
  # exe/lemans-remote pull-runs [RUN_ID ...] [--all] [--dry-run]
35
36
  # exe/lemans-remote drop-orphans [--min-age 10m]
36
37
  # exe/lemans-remote clobber RUN_ID ... | --all
@@ -213,11 +214,7 @@ module LemansRemote # :nodoc: all
213
214
 
214
215
  def pack(to:)
215
216
  Dir.mktmpdir("lemans-remote-stage") do |stage|
216
- @bench.root.children.each do |child|
217
- next if EXCLUDES.include?(child.basename.to_s)
218
-
219
- FileUtils.cp_r(child, File.join(stage, child.basename.to_s))
220
- end
217
+ stage_files(stage)
221
218
  prune_tasks(stage) if @selected_tasks
222
219
  _, err, status = Open3.capture3("tar", "-czf", to, "-C", stage, ".")
223
220
  raise "could not pack the bench: #{err}" unless status.success?
@@ -227,6 +224,44 @@ module LemansRemote # :nodoc: all
227
224
 
228
225
  private
229
226
 
227
+ # Every sandbox gets a copy, so a git bench ships what git would: tracked
228
+ # files plus untracked ones .gitignore lets through.
229
+ def stage_files(stage)
230
+ files = git_files
231
+ return copy_children(stage) unless files
232
+
233
+ files.each do |relative|
234
+ next if EXCLUDES.include?(relative.split("/", 2).first)
235
+
236
+ source = @bench.root.join(relative)
237
+ # The index still lists a file deleted from disk
238
+ next unless File.exist?(source) || File.symlink?(source)
239
+
240
+ destination = File.join(stage, relative)
241
+ FileUtils.mkdir_p(File.dirname(destination))
242
+ FileUtils.copy_entry(source, destination)
243
+ end
244
+ end
245
+
246
+ def copy_children(stage)
247
+ @bench.root.children.each do |child|
248
+ next if EXCLUDES.include?(child.basename.to_s)
249
+
250
+ FileUtils.cp_r(child, File.join(stage, child.basename.to_s))
251
+ end
252
+ end
253
+
254
+ # nil when the bench is not a git checkout; a nested repository lists as a
255
+ # bare directory, and is left out
256
+ def git_files
257
+ out, _err, status = Open3.capture3(
258
+ "git", "-C", @bench.root.to_s, "ls-files", "-z", "--cached", "--others", "--exclude-standard"
259
+ )
260
+ return nil unless status.success?
261
+
262
+ out.split("\0").reject { it.end_with?("/") }
263
+ end
264
+
230
265
  def prune_tasks(stage)
231
266
  tasks_rel = @bench.tasks_dir.relative_path_from(@bench.root).to_s
232
267
  staged_tasks = File.join(stage, tasks_rel)
@@ -415,6 +450,14 @@ module LemansRemote # :nodoc: all
415
450
  body.empty? ? [] : JSON.parse(body)
416
451
  end
417
452
 
453
+ def run_log(sandbox, run_id)
454
+ remote = "/vault/#{run_id}/run.log"
455
+ response = sandbox.process.exec(command: "cat #{Shellwords.escape(remote)}", timeout: 120)
456
+ raise "no log in the vault for #{run_id} (never launched, or still running on a sandbox that is gone)" unless response.exit_code.zero?
457
+
458
+ response.result.to_s
459
+ end
460
+
418
461
  def archive_size(sandbox, run_id)
419
462
  remote = "/vault/#{run_id}/runs.tar.gz"
420
463
  response = sandbox.process.exec(command: "du -h #{Shellwords.escape(remote)}", timeout: 60)
@@ -467,6 +510,7 @@ module LemansRemote # :nodoc: all
467
510
  SETUP_TIMEOUT_SEC = 300
468
511
  TRANSFER_TIMEOUT_SEC = 900
469
512
  TIMED_OUT = 124
513
+ CREATE_ATTEMPTS = 3
470
514
 
471
515
  def initialize(bench:, snapshot:, run_id:, tasks:, models:, extra_args:, extra_env:,
472
516
  timeout_sec:, runs_dir:, keep:, sync:, shell:)
@@ -518,8 +562,38 @@ module LemansRemote # :nodoc: all
518
562
  0
519
563
  end
520
564
 
565
+ # Daytona sometimes never starts a sandbox it accepted; the SDK gives up
566
+ # after a minute and the half-made sandbox stays behind under this run's
567
+ # labels, listed as running until Daytona flags it, then stale forever.
521
568
  def create_sandbox(volume = nil)
522
- params = ::Daytona::CreateSandboxFromSnapshotParams.new(
569
+ attempt = 0
570
+ begin
571
+ attempt += 1
572
+ LemansRemote.client.create(sandbox_params(volume))
573
+ rescue *Backend::Retries::SDK_ERRORS => e
574
+ drop_half_created
575
+ raise if attempt >= CREATE_ATTEMPTS
576
+
577
+ say :retry, "sandbox did not start (#{e.message}), attempt #{attempt + 1}/#{CREATE_ATTEMPTS}", :yellow
578
+ retry
579
+ end
580
+ end
581
+
582
+ # Runs from the rescue of a failed creation, so it must not raise: an
583
+ # exception here would replace the creation error and end the retries.
584
+ def drop_half_created
585
+ query = ::Daytona::ListSandboxesQuery.new(labels: { RUN_ID_LABEL => @run_id })
586
+ LemansRemote.client.list(query).each do |sandbox|
587
+ sandbox.delete
588
+ rescue StandardError => e
589
+ warn "lemans-remote: could not delete half-created sandbox #{sandbox.id}: #{e.message}"
590
+ end
591
+ rescue StandardError => e
592
+ warn "lemans-remote: could not list the sandboxes of #{@run_id}: #{e.message}"
593
+ end
594
+
595
+ def sandbox_params(volume)
596
+ ::Daytona::CreateSandboxFromSnapshotParams.new(
523
597
  snapshot: @snapshot,
524
598
  env_vars: env_vars,
525
599
  labels: {
@@ -533,7 +607,6 @@ module LemansRemote # :nodoc: all
533
607
  ttl_minutes: (@timeout_sec / 60.0).ceil + 60,
534
608
  volumes: volume ? [ Vault.mount_param(volume) ] : nil
535
609
  )
536
- LemansRemote.client.create(params)
537
610
  end
538
611
 
539
612
  def env_vars
@@ -897,6 +970,26 @@ module LemansRemote # :nodoc: all
897
970
  raise Thor::Error, "lemans-remote: #{e.message}"
898
971
  end
899
972
 
973
+ desc "logs RUN_ID", "Print a run's lemans output (live from its sandbox, or from the vault once it finished)"
974
+ option :tail, type: :numeric, desc: "Only the last N lines"
975
+ def logs(run_id)
976
+ row = Fleet.rows.find { it.run_id == run_id }
977
+ text =
978
+ if row&.state == DaytonaApiClient::SandboxState::STARTED
979
+ row.sandbox.process.exec(command: "cat #{REMOTE_LOG} 2>/dev/null", timeout: 60).result.to_s
980
+ elsif row && Fleet::LIVE_STATES.include?(row.state)
981
+ # The vault gets the log when the run exits, and a sandbox still
982
+ # coming up has nothing to read yet
983
+ raise "#{run_id} is #{row.state} — it has not started logging yet"
984
+ else
985
+ with_vault { |vault, sandbox| vault.run_log(sandbox, run_id) }
986
+ end
987
+ text = text.lines.last(options[:tail].to_i).join if options[:tail]
988
+ say text
989
+ rescue RuntimeError => e
990
+ raise Thor::Error, "lemans-remote: #{e.message}"
991
+ end
992
+
900
993
  map "pull-runs" => :pull_runs
901
994
  desc "pull-runs [RUN_IDS...]", "Download archived runs from the vault into the local runs directory"
902
995
  option :all, type: :boolean, default: false, desc: "Pull every completed run, even ones already pulled"
@@ -14,8 +14,9 @@ module Lemans
14
14
  NAME = "miniswen-installed"
15
15
  RESULTS_PATH = "/tmp/lemans-miniswen.result.json"
16
16
  INSTALL_TIMEOUT_SEC = 300
17
- # The CLI enforces max-time itself; the slack only covers process
18
- # startup, so the results file exists before the outer exec expires.
17
+ # The CLI enforces max-time itself, but between steps only: a command
18
+ # started just before the deadline runs to its own exec timeout first,
19
+ # and the outer exec must outlast that too. The slack covers startup.
19
20
  EXEC_SLACK_SEC = 60
20
21
 
21
22
  def install(_task, environment)
@@ -31,8 +32,7 @@ module Lemans
31
32
  # An in-sandbox run self-reports: everything but the verifier's reward
32
33
  # comes from a file the sandbox wrote.
33
34
  def obtain_result(task, environment)
34
- run = environment.exec(command_for(task), timeout: profile.timeout + EXEC_SLACK_SEC,
35
- env: provider_env(environment))
35
+ run = environment.exec(command_for(task), timeout: outer_timeout, env: provider_env(environment))
36
36
 
37
37
  begin
38
38
  Tempfile.create(%w[miniswen .result.json]) do |file|
@@ -49,6 +49,8 @@ module Lemans
49
49
 
50
50
  attr_reader :raw_result
51
51
 
52
+ def outer_timeout = profile.timeout + profile.exec_timeout + EXEC_SLACK_SEC
53
+
52
54
  # A missing credential fails the run before the sandbox executes
53
55
  # anything: it is the operator's configuration to fix, not a trial result.
54
56
  def provider_env(environment)
data/lib/lemans/trial.rb CHANGED
@@ -77,6 +77,11 @@ module Lemans
77
77
  rescue InfrastructureError, ::Miniswen::InfrastructureError => e
78
78
  # Mark the failure here, where the agent phase is still known
79
79
  result.failed!(:agent_error, e.message)
80
+ collect_patch!
81
+ raise
82
+ rescue ::Miniswen::AccountingError
83
+ # Classified by the outer rescue; the work is still on disk
84
+ collect_patch!
80
85
  raise
81
86
  end
82
87
 
@@ -86,6 +91,7 @@ module Lemans
86
91
 
87
92
  if response.error?
88
93
  result.failed!(:agent_error, response.error)
94
+ collect_patch!
89
95
  return result
90
96
  end
91
97
 
@@ -97,7 +103,7 @@ module Lemans
97
103
 
98
104
  check_cost_limit!
99
105
 
100
- patch.collect!(result, store, path: with_step_index("agent.patch")) if store
106
+ collect_patch!
101
107
  if step_task.final_step?
102
108
  patch.compile!(result, store) if task.multistep? && store
103
109
  # Don't index the final verification
@@ -146,6 +152,10 @@ module Lemans
146
152
 
147
153
  private
148
154
 
155
+ def collect_patch!
156
+ patch.collect!(result, store, path: with_step_index("agent.patch")) if store
157
+ end
158
+
149
159
  def save_trajectory!(trajectory)
150
160
  return unless trajectory && store
151
161
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lemans
4
- VERSION = "1.3.1"
4
+ VERSION = "1.3.2"
5
5
  end
@@ -8,9 +8,9 @@ RubyLLM.configure do |config|
8
8
  config.logger = Logger.new(IO::NULL) unless ENV["MINISWEN_DEBUG"] == "1"
9
9
  end
10
10
 
11
- # Increase retry window to handle egress network issues
11
+ # About a minute of retries for egress blips (1, 2, 4, 8, 16, 32s plus jitter)
12
12
  RubyLLM.configure do |config|
13
- config.max_retries = 3
13
+ config.max_retries = 6
14
14
  config.retry_interval = 1
15
15
  end
16
16
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Miniswen
4
- VERSION = "1.3.1"
4
+ VERSION = "1.3.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lemans
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov